git ssb

0+

Dominic / ssb-peer-invites



Tree: 3ae54f99cc027b146e10412677774f5cb735d2aa

Files: 3ae54f99cc027b146e10412677774f5cb735d2aa / index.js

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

Built with git-ssb-web