Files: 84e6e4f3bfd68a6cfbb34b0476ec2099244ec2c6 / index.js
10488 bytesRaw
1 | var pull = require('pull-stream') |
2 | var Reduce = require('flumeview-reduce') |
3 | var I = require('./valid') |
4 | |
5 | function code(err, c) { |
6 | err.code = 'user-invites:'+c |
7 | return err |
8 | } |
9 | |
10 | function all (stream, cb) { |
11 | return pull(stream, pull.collect(cb)) |
12 | } |
13 | |
14 | function isFunction (f) { |
15 | return typeof f === 'function' |
16 | } |
17 | |
18 | exports.name = 'user-invites' |
19 | |
20 | exports.version = '1.0.0' |
21 | exports.manifest = { |
22 | getInvite: 'async', |
23 | confirm: 'async', |
24 | create: 'async' |
25 | } |
26 | |
27 | exports.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 | |
39 | exports.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