git ssb

0+

Dominic / ssb-peer-invites



Tree: 8a9681abe2e2be89ddcfdfa642696014bfbe39f4

Files: 8a9681abe2e2be89ddcfdfa642696014bfbe39f4 / index.js

12071 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 getNearbyPubs(opts, function (err, near) {
279 var seed = crypto.randomBytes(32)
280 sbot.identities.publishAs({
281 id: opts.id || sbot.id,
282 content: I.createInvite(seed, opts.id || sbot.id, opts.reveal, opts.private, caps)
283 }, function (err, data) {
284 cb(null, {
285 seed: seed,
286 invite: data.key,
287 pubs: near,
288 })
289 })
290 })
291 }
292
293 //try each of an array of addresses, and cb the first one that works.
294 function connectFirst (keys, pubs, cb) {
295 var n = 0, err
296 pubs.forEach(function (addr) {
297 n++
298 ssbClient(keys, {
299 remote: addr,
300 caps: caps,
301 manifest: {
302 userInvites: {
303 getInvite: 'async',
304 confirm: 'async'
305 }
306 }
307 }, function (_err, rpc) {
308 if(n > 0 && rpc) {
309 n = -1
310 cb(null, rpc)
311 } else {
312 err = err || _err
313 }
314 if(--n == 0) cb(err)
315 })
316 })
317 }
318
319 //TODO: check if invite is already held locally
320 // if so, just get it. when got, update local db.
321 invites.openInvite = function (invite, cb) {
322 invites.getInvite(invite.invite, function (err, msg) {
323 if(msg)
324 next(msg)
325 else {
326 var pubs = invite.pubs
327 var keys = ssbKeys.generate(null, invite.seed)
328 connectFirst(keys, pubs, function (err, rpc) {
329 if(err) return cb(err)
330 rpc.userInvites.getInvite(invite.invite, function (err, msg) {
331 if(err) return cb(err)
332 next(msg)
333 })
334 })
335 }
336
337 function next (msg) {
338 var invite_id = '%'+ssbKeys.hash(JSON.stringify(msg, null, 2))
339 if(invite.invite !== invite_id)
340 return cb(new Error(
341 'incorrect invite was returned! expected:'+invite.invite+', but got:'+inviteId
342 ))
343 var opened
344 try { opened = I.verifyInvitePrivate(msg, invite.seed, caps) }
345 catch (err) { return cb(err) }
346 //UPDATE REDUCE STATE.
347 // this is a wee bit naughty, because if you rebuild the index it might not have this invite
348 // (until you replicate it, but when you do the value won't change)
349 state.set(reduce(state.value, {key: invite_id, value:msg}, invites.since.value))
350 cb(null, msg, opened)
351 }
352 })
353 }
354
355 invites.acceptInvite = function (opts, cb) {
356 var invite = isObject(opts.invite) ? opts.invite : opts
357 var invite_id = invite.invite
358 var id = opts.id || sbot.id
359 var pubs = invite.pubs
360 var keys = ssbKeys.generate(null, invite.seed)
361
362 //check wether this invite is already accepted.
363 //or if the acceptance has been publish, but not yet confirmed.
364 getAccept(invite_id, function (err, accept) {
365 if(accept) next(accept)
366 else {
367 invites.openInvite(invite, function (err, invite_msg, opened) {
368 sbot.identities.publishAs({
369 id: id,
370 content: I.createAccept(invite_msg, invite.seed, id, caps)
371 }, function (err, accept) {
372 if(err) cb(err)
373 else {
374 state.set(reduce(state.value, accept, invites.since.value))
375 next(accept.value)
376 }
377 })
378 })
379 }
380 })
381
382 function next(accept) {
383 getConfirm(invite_id, accept, function (err, confirm) {
384 if(!confirm)
385 var pubs = invite.pubs
386 var keys = ssbKeys.generate(null, invite.seed)
387 connectFirst(keys, pubs, function (err, rpc) {
388 if(err) return cb(err)
389 rpc.userInvites.confirm(accept, function (err, confirm) {
390 //TODO: store confirms for us in the state.
391 cb(err, confirm)
392 })
393 })
394 })
395 }
396 }
397 return invites
398}
399
400
401

Built with git-ssb-web