git ssb

0+

Dominic / ssb-peer-invites



Tree: 0d533f2e86c275fe20ee7d30ad22e15f5e6c944c

Files: 0d533f2e86c275fe20ee7d30ad22e15f5e6c944c / index.js

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

Built with git-ssb-web