git ssb

0+

Dominic / ssb-peer-invites



Tree: 95bf472d83534207a4906581c2c874d88e070605

Files: 95bf472d83534207a4906581c2c874d88e070605 / index.js

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

Built with git-ssb-web