git ssb

0+

Dominic / ssb-peer-invites



Tree: 1a81a73eb9805d4f447fee3a6ce4887561d9df86

Files: 1a81a73eb9805d4f447fee3a6ce4887561d9df86 / index.js

12116 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 caps.userInvite = caps.userInvite || require('./cap')
54
55 function reduce (acc, data, _seq) {
56 if(!acc) acc = {invited: {}, invites:{}, accepts: {}, hosts: {}}
57 var msg = data.value
58 var invite, accept
59 if(types.isInvite(msg, caps)) {
60 //TODO: validate that this is a msg we understand!
61 invite = msg
62 accept = acc.accepts[data.key]
63 }
64 else if(types.isAccept(msg, caps)) {
65 accept = msg
66 invite = acc.invites[accept.content.receipt]
67 }
68 else if(types.isConfirm(msg, caps)) {
69 //TODO: just for when we are the guest, but we need to make sure at least one confirm exists.
70 accept = msg.content.embed
71 invite = acc.invites[accept.content.receipt]
72 }
73
74 if(invite && accept) {
75 if(invite === true)
76 return acc
77 var invite_id = accept.content.receipt
78 try { I.verifyAccept(accept, invite, caps) }
79 catch (err) { return acc }
80 //fall through from not throwing
81
82 //delete matched invites, but _only_ if they are VALID. (returned in the catch if invalid)
83 delete acc.accepts[invite_id]
84 //but remember that this invite has been processed.
85 acc.invites[invite_id] = true
86 acc.hosts[invite.author] = acc.hosts[invite.author] || {}
87 acc.hosts[invite.author][accept.author] = 1
88 if(init) {
89 //interpret accepting an invite as a mutual follow.
90 layer(invite.author, accept.author, 1)
91 layer(accept.author, invite.author, 1)
92 }
93 }
94 else if(invite)
95 acc.invites[data.key] = invite
96 else if(accept)
97 acc.accepts[accept.receipt] = accept
98
99 return acc
100 }
101
102 var initial = {invites: {}, accepts: {}, hosts: {}}
103 var state
104 //a hack here, so that we can grab a handle on invites.value.set
105 var invites = sbot._flumeUse('user-invites', function (log, name) {
106 var _invites = Reduce(2, reduce, null, null, initial)(log, name)
107 state = _invites.value
108 return _invites
109 })
110
111 invites.get(function (_, invites) {
112 var g = {}
113 if(!invites) layer({})
114 else {
115 //interpret accepted invites as two-way, but only store a minimal host->guest data structure
116 for(var j in invites.hosts)
117 for(var k in invites.hosts[j])
118 g[j][k] = g[k][j] = 1
119 init = true
120 layer(g)
121 }
122 })
123
124 sbot.auth.hook(function (fn, args) {
125 var id = args[0], cb = args[1]
126 invites.get(function (err, v) {
127 if(err) return cb(err)
128 for(var k in v.invites) {
129 if(v.invites[k].content.invite === id) {
130 return cb(null, {
131 allow: ['userInvites.getInvite', 'userInvites.confirm'],
132 deny: null
133 })
134 }
135 }
136 fn.apply(null, args)
137 })
138 })
139
140 //retrive full invitation.
141 invites.getInvite = function (invite_id, cb) {
142 var self = this
143 invites.get(function (err, v) {
144 var invite = v.invites[invite_id]
145 if(err) return cb(err)
146 if(!invite)
147 cb(code(
148 new Error('unknown invite:'+invite_id),
149 'unknown-invite'
150 ))
151 else if(invite === true)
152 //TODO just retrive all confirmations we know about
153 //via links.
154 sbot.get(invite_id, cb)
155 //only allow the guest to request their own invite.
156 else if(self.id !== invite.content.invite)
157 cb(code(
158 new Error('invite did not match client id'),
159 'invite-mismatch'
160 ))
161 else
162 cb(null, v.invites[invite_id])
163 })
164 }
165
166 function getResponse (invite_id, test, cb) {
167 return all(
168 sbot.links({dest: invite_id, values: true, keys: false, meta: false}),
169 function (err, confirms) {
170 if(err) cb(err)
171 else cb(null,
172 confirms.filter(function (e) {
173 try {
174 return test(e)
175 } catch (err) {
176 return false
177 }
178 })[0]
179 )
180 }
181 )
182 }
183
184 var accepted = {}
185
186 function getConfirm (invite_id, accept, cb) {
187 getResponse(invite_id, function (msg) {
188 return (
189 msg.content.type === 'user-invite/confirm' &&
190 msg.content.embed.content.receipt === invite_id &&
191 deepEquals(msg.content.embed, accept)
192 )
193 }, cb)
194 }
195
196
197 function getAccept (invite_id, cb) {
198 getResponse(invite_id, function (msg) {
199 return (
200 msg.content.type === 'user-invite/accept' &&
201 msg.content.receipt === invite_id
202 )
203 }, cb)
204 }
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({
227 type: 'user-invite/confirm',
228 embed: accept,
229 //second pointer back to receipt, so that links can find it
230 //(since it unfortunately does not handle links nested deeper
231 //inside objects. when we look up the message,
232 //confirm that content.embed.content.receipt is the same)
233 receipt: accept.content.receipt
234 }, 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

Built with git-ssb-web