git ssb

0+

Dominic / ssb-peer-invites



Tree: b6540cc83c887c7a955b7a6e3e2280c017951e30

Files: b6540cc83c887c7a955b7a6e3e2280c017951e30 / index.js

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

Built with git-ssb-web