git ssb

0+

Dominic / ssb-peer-invites



Tree: 74fdd566bc891a78ae65fcc6e0220fdcbbde4e21

Files: 74fdd566bc891a78ae65fcc6e0220fdcbbde4e21 / index.js

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

Built with git-ssb-web