git ssb

0+

Dominic / ssb-peer-invites



Tree: e7229dc42da556bddba6c1a1c0a6e091ca8efb7d

Files: e7229dc42da556bddba6c1a1c0a6e091ca8efb7d / index.js

15320 bytesRaw
1var pull = require('pull-stream')
2var Reduce = require('flumeview-reduce')
3var I = require('./valid')
4var deepEquals = require('deep-equals')
5var types = require('./types')
6var paramap = require('pull-paramap')
7var ssbClient = require('ssb-client')
8var crypto = require('crypto')
9var ssbKeys = require('ssb-keys')
10var u = require('./util')
11
12function code(err, c) {
13 err.code = 'user-invites:'+c
14 return err
15}
16
17function all (stream, cb) {
18 return pull(stream, pull.collect(cb))
19}
20
21function isFunction (f) {
22 return typeof f === 'function'
23}
24
25function isObject (o) {
26 return o && typeof o == 'object'
27}
28
29function isString (s) {
30 return typeof s == 'string'
31}
32
33function toBuffer(b) {
34 return Buffer.isBuffer(b) ? b : Buffer.from(b, 'base64')
35}
36
37exports.name = 'user-invites'
38
39exports.version = '1.0.0'
40exports.manifest = {
41 getInvite: 'async',
42 confirm: 'async',
43 create: 'async',
44 willReplicate: 'async',
45 getNearbyPubs: 'async',
46 openInvite: 'async',
47 acceptInvite: 'async'
48}
49
50exports.permissions = {
51 anonymous: {allow: ['willReplicate']},
52}
53
54// KNOWN BUG: it's possible to accept an invite more than once,
55// but peers will ignore subsequent acceptances. it's possible
56// that this could create confusion in certain situations.
57// (but you'd get a feed that some peers thought was invited by Alice
58// other peers would think a different feed accepted that invite)
59// I guess the answer is to let alice reject the second invite?)
60// that would be easier to do if this was a levelreduce? (keys: reduce, instead of a single reduce?)
61
62exports.init = function (sbot, config) {
63 var init = false
64 var layer = sbot.friends.createLayer('user-invites')
65
66 var caps = config.caps || {}
67 caps.userInvite = caps.userInvite || require('./cap')
68 var initial = {invites: {}, accepts: {}, hosts: {}, guests: {}}
69
70 function reduce (acc, data, _seq) {
71 if(!acc) acc = initial
72 var msg = data.value
73 var invite, accept
74 if(types.isInvite(msg, caps)) {
75 //TODO: validate that this is a msg we understand!
76 invite = msg
77 accept = acc.accepts[data.key]
78
79 //remember guest ids, so that you can process pub messages.
80 //this is necessary to confirm invites here the guest failed before they received
81 //the confirmation, and do not realize they are confirmed yet. they'll try again later.
82
83 //id rather not have this here. it's gonna bloat. better a different kind of index.
84 //can fix this later though.
85 acc.guests[data.value.content.invite] = true
86 }
87 else if(types.isAccept(msg, caps)) {
88 accept = msg
89 invite = acc.invites[accept.content.receipt]
90 }
91 else if(types.isConfirm(msg, caps)) {
92 //TODO: just for when we are the guest, but we need to make sure at least one confirm exists.
93 accept = msg.content.embed
94 invite = acc.invites[accept.content.receipt]
95 }
96
97 if(invite && accept) {
98 if(invite === true)
99 return acc
100 var invite_id = accept.content.receipt
101 try { I.verifyAccept(accept, invite, caps) }
102 catch (err) { return acc }
103 //fall through from not throwing
104
105 //delete matched invites, but _only_ if they are VALID. (returned in the catch if invalid)
106 delete acc.accepts[invite_id]
107 //but remember that this invite has been processed.
108 acc.invites[invite_id] = true
109 acc.hosts[invite.author] = acc.hosts[invite.author] || {}
110 acc.hosts[invite.author][accept.author] = 1
111 if(init) {
112 //interpret accepting an invite as a mutual follow.
113 layer(invite.author, accept.author, 1)
114 layer(accept.author, invite.author, 1)
115 }
116 }
117 else if(invite)
118 acc.invites[data.key] = invite
119 else if(accept)
120 acc.accepts[accept.receipt] = accept
121
122 return acc
123 }
124
125 var state
126 //a hack here, so that we can grab a handle on invites.value.set
127 var invites = sbot._flumeUse('user-invites', function (log, name) {
128 var _invites = Reduce(3, reduce, null, null, initial)(log, name)
129 state = _invites.value
130 return _invites
131 })
132
133 invites.get(function (_, invites) {
134 var g = {}
135 if(!invites) layer({})
136 else {
137 //interpret accepted invites as two-way, but only store a minimal host->guest data structure
138 for(var j in invites.hosts)
139 for(var k in invites.hosts[j])
140 g[j][k] = g[k][j] = 1
141 init = true
142 layer(g)
143 }
144 })
145
146 sbot.auth.hook(function (fn, args) {
147 var id = args[0], cb = args[1]
148 //currently a problem here where message may be confirmed,
149 //but guest didn't get the welcome yet. they need to be able to connect
150 //and request it again.
151 invites.get(function (err, v) {
152 if(err) return cb(err)
153 if(v.guests[id])
154 return cb(null, {
155 allow: ['userInvites.getInvite', 'userInvites.confirm'],
156 deny: null
157 })
158 fn.apply(null, args)
159 })
160 })
161
162 //retrive full invitation.
163 invites.getInvite = function (invite_id, cb) {
164 var self = this
165 invites.get(function (err, v) {
166 var invite = v.invites[invite_id]
167 if(err) return cb(err)
168 if(!invite)
169 cb(code(
170 new Error('unknown invite:'+invite_id),
171 'unknown-invite'
172 ))
173 else if(invite === true)
174 //TODO just retrive all confirmations we know about
175 //via links.
176 sbot.get(invite_id, cb)
177 //only allow the guest to request their own invite.
178 else if(self.id !== invite.content.invite)
179 cb(code(
180 new Error('invite did not match client id'),
181 'invite-mismatch'
182 ))
183 else
184 cb(null, v.invites[invite_id])
185 })
186 }
187
188 function getResponse (invite_id, test, cb) {
189 return all(
190 sbot.links({dest: invite_id, values: true, keys: false, meta: false}),
191 function (err, confirms) {
192 if(err) cb(err)
193 else cb(null,
194 confirms.filter(function (e) {
195 try {
196 return test(e)
197 } catch (err) {
198 return false
199 }
200 })[0]
201 )
202 }
203 )
204 }
205
206 var accepted = {}
207
208 function getConfirm (invite_id, accept, cb) {
209 getResponse(invite_id, function (msg) {
210 return (
211 msg.content.type === 'user-invite/confirm' &&
212 msg.content.embed.content.receipt === invite_id &&
213 deepEquals(msg.content.embed, accept)
214 )
215 }, cb)
216 }
217
218 //used to request that a server confirms your acceptance.
219 invites.confirm = function (accept, cb) {
220 var invite_id = accept.content.receipt
221 //check if the invite in question hasn't already been accepted.
222 getConfirm(invite_id, accept, function (err, confirm) {
223 if(err) return cb(err)
224 if(confirm) return cb(null, confirm)
225
226 sbot.get(invite_id, function (err, invite) {
227 try {
228 I.verifyAccept(accept, invite, caps)
229 } catch (err) {
230 return cb(err)
231 }
232 //there is a little race condition here, if accept is called again
233 //before this write completes, it will write twice, so just return an error.
234 if(accepted[invite_id]) return cb(new Error('race condition: try again soon'))
235
236 accepted[invite_id] = true
237 sbot.publish(I.createConfirm(accept), function (err, data) {
238 delete accepted[invite_id]
239 cb(err, data.value)
240 })
241 })
242 })
243 }
244
245
246 //if the caller is someone we know, let them know wether
247 //we are willing to confirm (and replicate) their guest.
248 invites.willReplicate = function (opts, cb) {
249 if(isFunction(opts)) cb = opts, opts = {}
250 var id = this.id //id of caller
251 var max = config.friends && config.friends.hops || config.replicate && config.replicate.hops || 3
252 sbot.friends.hops({}, function (err, hops) {
253 // compare hops of caller (host to be) with max - 1
254 // because that means that the hops of the guest
255 // will be in range.
256 if(hops[id] <= (max - 1)) cb(null, true)
257 else cb(null, false)
258 })
259 }
260
261 function getAccept (invite_id, cb) {
262 getResponse(invite_id, function (msg) {
263 return (
264 msg.content.type === 'user-invite/accept' &&
265 msg.content.receipt === invite_id
266 )
267 }, cb)
268 }
269
270 //retrive pubs who might be willing to confirm your invite. (used when creating an invte)
271 invites.getNearbyPubs = function (opts, cb) {
272 if(isFunction (opts))
273 cb = opts, opts = {}
274 var maxHops = opts.hops || 2
275 sbot.deviceAddress.getState(function (err, state) {
276 if(err) return cb(explain(err, 'could not retrive any device addresses'))
277 sbot.friends.hops({hops: opts.hops, reverse: true, start: opts.id}, function (err, hops) {
278 if(err) return cb(explain(err, 'could not retrive nearby friends'))
279 var near = []
280 for(var k in state) {
281 var da = state[k]
282 if(hops[k] <= maxHops) {
283 near.push({
284 id: k,
285 address: da.address,
286 hops: hops[k],
287 availability: da.availability
288 })
289 }
290 }
291 //sort by reverse hops, then by decending availability.
292 //default availibility
293 near.sort(function (a, b) {
294 return (
295 a.hops - b.hops ||
296 b.availability - a.availability
297 )
298 })
299
300 if(opts.offline) return cb(null, near)
301
302 var count = 3, found = []
303
304 function pushFound (pub, err, will) {
305 found.push({
306 id: pub.id, address: pub.address,
307 availability: pub.availability,
308 hops: pub.hops,
309 error: err && err.message, willReplicate: !!will
310 })
311 if(will) count --
312 //sort in order of wether they will replicate,
313 //or availability
314 found.sort(function (a, b) {
315 (!!b.willReplicate) - (!!a.willReplicate) || b.availability - a.availability
316 })
317 }
318
319 pull(
320 pull.values(near),
321 paramap(function (pub, cb) {
322 //if opts.id != sbot.id connect using ssb client
323 //so that you ask willReplicate from the correct id.
324 sbot.connect(pub.address, function (err, rpc) {
325 //skip pubs that were not contactable
326 if(err) {
327 pushFound(pub, err)
328 return cb()
329 }
330 rpc.userInvites.willReplicate({}, function (err, v) {
331 //pass through input if true, else (err or false)
332 //then drop.
333 pushFound(pub, err, !!v)
334 cb(null, v && pub)
335 })
336 })
337 },3),
338 function (read) {
339 read(null, function next (err, pub) {
340 if(err) return cb(null, found)
341 else if(count) read(null, next)
342 else read(true, function (_) { cb(null, found) })
343 })
344 }
345 )
346 })
347 })
348 }
349
350 invites.create = function (opts, cb) {
351 if(isFunction(opts))
352 return opts(new Error ('user-invites: expected: options *must* be provided.'))
353
354 var host_id = opts.id || sbot.id
355 invites.getNearbyPubs(opts, function (err, near) {
356 if(near.length == 0 && !opts.allowWithoutPubs)
357 return cb(new Error('failed to find any suitable pubs'))
358
359 var seed = crypto.randomBytes(32).toString('base64')
360 sbot.identities.publishAs({
361 id: host_id,
362 content: I.createInvite(seed, host_id, opts.reveal, opts.private, caps)
363 }, function (err, data) {
364 if(err) return cb(err)
365 var invite = {
366 seed: seed,
367 invite: data.key,
368 cap: opts.cap,
369 pubs: near.map(function (e) { return e.address }),
370 }
371 cb(null, u.stringify(invite))
372 })
373 })
374 }
375
376 //try each of an array of addresses, and cb the first one that works.
377 function connectFirst (invite, cb) {
378 var n = 0, err
379 var keys = ssbKeys.generate(null, toBuffer(invite.seed))
380 invite.pubs.forEach(function (addr) {
381 n++
382 //don't use sbot.connect here, because we are connecting
383 //with a different cap.
384 ssbClient(keys, {
385 remote: addr,
386 caps: {shs: invite.cap || caps.shs},
387 manifest: {
388 userInvites: {
389 getInvite: 'async',
390 confirm: 'async'
391 }
392 }
393 }, function (_err, rpc) {
394 if(n > 0 && rpc) {
395 n = -1
396 cb(null, rpc)
397 } else {
398 err = err || _err
399 }
400 if(--n == 0) cb(err)
401 })
402 })
403 }
404
405 //TODO: check if invite is already held locally
406 // if so, just get it. when got, update local db.
407 invites.openInvite = function (invite, cb) {
408 if(isString(invite)) invite = u.parse(invite)
409 invites.getInvite(invite.invite, function (err, msg) {
410 if(msg)
411 next(msg)
412 else
413 connectFirst(invite, function (err, rpc) {
414 if(err) return cb(err)
415 rpc.userInvites.getInvite(invite.invite, function (err, msg) {
416 if(err) return cb(err)
417 next(msg)
418 })
419 })
420
421 function next (msg) {
422 var invite_id = '%'+ssbKeys.hash(JSON.stringify(msg, null, 2))
423 if(invite.invite !== invite_id)
424 return cb(new Error(
425 'incorrect invite was returned! expected:'+invite.invite+', but got:'+inviteId
426 ))
427 var opened
428 try { opened = I.verifyInvitePrivate(msg, invite.seed, caps) }
429 catch (err) { return cb(err) }
430 //UPDATE REDUCE STATE.
431 // this is a wee bit naughty, because if you rebuild the index it might not have this invite
432 // (until you replicate it, but when you do the value won't change)
433 state.set(reduce(state.value, {key: invite_id, value:msg}, invites.since.value))
434 cb(null, msg, opened)
435 }
436 })
437 }
438
439 invites.acceptInvite = function (opts, cb) {
440 if(isString(opts)) opts = u.parse(opts)
441 var invite = isObject(opts.invite) ? opts.invite : opts
442 var invite_id = invite.invite
443 var id = opts.id || sbot.id
444 var pubs = invite.pubs
445 var keys = ssbKeys.generate(null, toBuffer(invite.seed))
446
447 //check wether this invite is already accepted.
448 //or if the acceptance has been publish, but not yet confirmed.
449 getAccept(invite_id, function (err, accept) {
450 if(accept) next(accept)
451 else {
452 invites.openInvite(invite, function (err, invite_msg, opened) {
453 sbot.identities.publishAs({
454 id: id,
455 content: I.createAccept(invite_msg, invite.seed, id, caps)
456 }, function (err, accept) {
457 if(err) cb(err)
458 else {
459 state.set(reduce(state.value, accept, invites.since.value))
460 next(accept.value)
461 }
462 })
463 })
464 }
465 })
466
467 function next(accept) {
468 getConfirm(invite_id, accept, function (err, confirm) {
469 if(!confirm)
470 connectFirst(invite, function (err, rpc) {
471 if(err) return cb(err)
472 rpc.userInvites.confirm(accept, function (err, confirm) {
473 //TODO: store confirms for us in the state.
474 cb(err, confirm)
475 })
476 })
477 })
478 }
479 }
480
481 return invites
482}
483
484// I am not happy with how big this file is
485// but I can't see a really good line along which to break it up.
486
487

Built with git-ssb-web