git ssb

0+

Dominic / ssb-peer-invites



Tree: 66c545deb943d0b40249722b3d1b125249d6bcde

Files: 66c545deb943d0b40249722b3d1b125249d6bcde / index.js

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

Built with git-ssb-web