git ssb

0+

Dominic / ssb-peer-invites



Tree: d8e63bc4e3cc6ca4565996990f626ed2a9742fd6

Files: d8e63bc4e3cc6ca4565996990f626ed2a9742fd6 / index.js

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

Built with git-ssb-web