Files: d185716fb11842271c37ff499725b2b66ba2a5e3 / lib / about.js
11931 bytesRaw
1 | var pull = require('pull-stream') |
2 | var multicb = require('multicb') |
3 | var cat = require('pull-cat') |
4 | var u = require('./util') |
5 | |
6 | module.exports = About |
7 | |
8 | function About(app, myId, follows) { |
9 | this.app = app |
10 | this.myId = myId |
11 | this.follows = follows |
12 | } |
13 | |
14 | About.prototype.createAboutOpStream = function (id) { |
15 | return pull( |
16 | this.app.sbot.links({dest: id, rel: 'about', values: true, reverse: true, private: true, meta: false}), |
17 | this.app.unboxMessages(), |
18 | pull.map(function (msg) { |
19 | var c = msg.value.content |
20 | if (typeof c !== 'object' || c === null) return [] |
21 | return Object.keys(c).filter(function (key) { |
22 | return key !== 'about' |
23 | && key !== 'type' |
24 | && key !== 'recps' |
25 | && key !== 'reason' |
26 | }).map(function (key) { |
27 | var value = c[key] |
28 | return { |
29 | id: msg.key, |
30 | author: msg.value.author, |
31 | timestamp: msg.value.timestamp, |
32 | prop: key, |
33 | value: value, |
34 | remove: value && value.remove, |
35 | } |
36 | }) |
37 | }), |
38 | pull.flatten() |
39 | ) |
40 | } |
41 | |
42 | About.prototype.createAboutStreams = function (id) { |
43 | var ops = this.createAboutOpStream(id) |
44 | var scalars = {/* author: {prop: value} */} |
45 | var sets = {/* author: {prop: {link}} */} |
46 | |
47 | var setsDone = multicb({pluck: 1, spread: true}) |
48 | setsDone()(null, pull.values([])) |
49 | return { |
50 | scalars: pull( |
51 | ops, |
52 | pull.unique(function (op) { |
53 | return op.author + '-' + op.prop + '-' |
54 | }), |
55 | pull.filter(function (op) { |
56 | return !op.remove |
57 | }) |
58 | ), |
59 | sets: u.readNext(setsDone) |
60 | } |
61 | } |
62 | |
63 | function computeTopAbout(aboutByFeed) { |
64 | var propValueCounts = {/* prop: {value: count} */} |
65 | var topValues = {/* prop: value */} |
66 | var topValueCounts = {/* prop: count */} |
67 | for (var feed in aboutByFeed) { |
68 | var feedAbout = aboutByFeed[feed] |
69 | for (var prop in feedAbout) { |
70 | var value = feedAbout[prop] |
71 | var valueCounts = propValueCounts[prop] || (propValueCounts[prop] = {}) |
72 | var count = (valueCounts[value] || 0) + 1 |
73 | valueCounts[value] = count |
74 | if (count > (topValueCounts[prop] || 0)) { |
75 | topValueCounts[prop] = count |
76 | topValues[prop] = value |
77 | } |
78 | } |
79 | } |
80 | return topValues |
81 | } |
82 | |
83 | About.prototype.get = function (dest, cb) { |
84 | if (dest[0] === '@') return this.getSocial(dest, cb) |
85 | return this.getCausal(dest, cb) |
86 | } |
87 | |
88 | function copy(obj) { |
89 | if (obj === null || typeof obj !== 'object') return obj |
90 | var o = {} |
91 | for (var k in obj) o[k] = obj[k] |
92 | return o |
93 | } |
94 | |
95 | function premapAbout(msg) { |
96 | var value = { |
97 | about: {}, |
98 | mentions: {}, |
99 | branches: {}, |
100 | sources: {}, |
101 | ts: msg.ts |
102 | } |
103 | var c = msg.value.content |
104 | if (!c) return value |
105 | |
106 | if (c.branch) { |
107 | msg.branches.map(function (id) { |
108 | value.branches[id] = true |
109 | }) |
110 | } |
111 | |
112 | if (!c.about && c.root) return value |
113 | value.about = {} |
114 | var author = msg.value.author |
115 | var source = {id: msg.key, seq: msg.value.sequence} |
116 | for (var k in c) switch(k) { |
117 | case 'type': |
118 | case 'about': |
119 | case 'branch': |
120 | break |
121 | case 'recps': |
122 | // get recps from root message only |
123 | if (!c.root && !c.about) { |
124 | value.recps = {} |
125 | u.toLinkArray(c.recps).forEach(function (link) { |
126 | value.recps[link.link] = link |
127 | }) |
128 | } |
129 | break |
130 | case 'mentions': |
131 | value.mentions = {} |
132 | u.toLinkArray(c.mentions).map(function (link) { |
133 | value.mentions[link.link] = link |
134 | }) |
135 | break |
136 | case 'image': |
137 | var link = u.toLink(c.image) |
138 | if (!link) break |
139 | value.about.image = link.link |
140 | value.about.imageLink = link |
141 | var sources = value.sources.image || (value.sources.image = []) |
142 | sources[author] = source |
143 | break |
144 | case 'attendee': |
145 | var attendee = u.linkDest(c.attendee) |
146 | if (attendee && attendee === author) { |
147 | // TODO: allow users adding other users as attendees? |
148 | var attendeeLink = copy(u.toLink(c.attendee)) |
149 | attendeeLink.source = msg.key |
150 | value.attending = {} |
151 | value.attending[attendee] = attendeeLink.remove ? null : attendeeLink } |
152 | break |
153 | default: |
154 | // TODO: handle arrays |
155 | value.about[k] = c[k] |
156 | var sources = value.sources[k] || (value.sources[k] = {}) |
157 | sources[author] = source |
158 | } |
159 | return value |
160 | } |
161 | |
162 | function reduceAbout(values, lastValue) { |
163 | var newValue = { |
164 | about: {}, |
165 | mentions: {}, |
166 | branches: {}, |
167 | sources: {} |
168 | } |
169 | values.sort(compareByTs).concat(lastValue).forEach(function (value) { |
170 | if (!value) return |
171 | if (value.ts) { |
172 | if (!newValue.ts || value.ts > newValue.ts) { |
173 | newValue.ts = value.ts |
174 | } |
175 | } |
176 | if (value.attending) { |
177 | var attending = newValue.attending || (newValue.attending = {}) |
178 | for (var k in value.attending) { |
179 | attending[k] = value.attending[k] |
180 | } |
181 | } |
182 | if (value.mentions) for (var k in value.mentions) { |
183 | newValue.mentions[k] = value.mentions[k] |
184 | } |
185 | if (value.branches) for (var k in value.branches) { |
186 | newValue.branches[k] = value.branches[k] |
187 | } |
188 | if (value.recps) { |
189 | // note: zero recps is still private. truthy recps indicates private |
190 | var recps = newValue.recps || (newValue.recps = {}) |
191 | for (var k in value.recps) { |
192 | newValue.recps[k] = value.recps[k] |
193 | } |
194 | } |
195 | if (value.about) for (var k in value.about) { |
196 | // TODO: use merge heuristics |
197 | newValue.about[k] = value.about[k] |
198 | if (lastValue && lastValue.about[k]) { |
199 | if (value === lastValue) { |
200 | newValue.sources[k] = lastValue.sources[k] |
201 | } else { |
202 | // message setting a property resets the property's sources from branches |
203 | } |
204 | } else { |
205 | var newSources = newValue.sources[k] || (newValue.sources[k] = {}) |
206 | var sources = value.sources[k] |
207 | for (var feed in sources) { |
208 | if (newSources[feed] && newSources[feed].seq > sources[feed].seq) { |
209 | // assume causal order in user's own feed. |
210 | // this condition shouldn't be reached if messages are in feed order |
211 | console.error('skip', k, feed, sources[feed].id, newSources[feed].id) |
212 | continue |
213 | } |
214 | newSources[feed] = sources[feed] |
215 | } |
216 | } |
217 | } |
218 | }) |
219 | return newValue |
220 | } |
221 | |
222 | function postmapAbout(value) { |
223 | var about = { |
224 | ts: value.ts, |
225 | _sources: {} |
226 | } |
227 | for (var k in value.sources) { |
228 | var propSources = about._sources[k] = [] |
229 | for (var feed in value.sources[k]) { |
230 | propSources.push(value.sources[k][feed].id) |
231 | } |
232 | } |
233 | if (value.mentions) { |
234 | about.mentions = [] |
235 | for (var k in value.mentions) { |
236 | about.mentions.push(value.mentions[k]) |
237 | } |
238 | } |
239 | if (value.attending) { |
240 | about.attendee = [] |
241 | for (var k in value.attending) { |
242 | var link = value.attending[k] |
243 | if (link) about.attendee.push(link) |
244 | } |
245 | } |
246 | if (value.branches) about.branch = Object.keys(value.branches) |
247 | if (value.recps) { |
248 | about.recps = [] |
249 | for (var k in value.recps) { |
250 | about.recps.push(value.recps[k]) |
251 | } |
252 | } |
253 | if (value.about) for (var k in value.about) { |
254 | about[k] = value.about[k] |
255 | } |
256 | return about |
257 | } |
258 | |
259 | function compareByTs(a, b) { |
260 | return a.ts - b.ts |
261 | } |
262 | |
263 | About.prototype.getCausal = function (dest, cb) { |
264 | if (!this.app.sbot.links) return pull.error(new Error('missing sbot.links')) |
265 | var self = this |
266 | var backlinks = {} |
267 | var seen = {} |
268 | var queue = [] |
269 | var aboutAtMsgs = {} |
270 | var now = Date.now() |
271 | function enqueue(msg) { |
272 | if (!seen[msg.key]) { |
273 | seen[msg.key] = true |
274 | queue.push(msg) |
275 | } |
276 | } |
277 | function isMsgIdDone(id) { |
278 | return !!aboutAtMsgs[id] |
279 | } |
280 | function isMsgReady(msg) { |
281 | return msg.branches.every(isMsgIdDone) |
282 | } |
283 | function dequeue() { |
284 | var msg = queue.filter(isMsgReady).sort(compareByTs)[0] |
285 | if (!msg) return console.error('thread error'), queue.shift() |
286 | var i = queue.indexOf(msg) |
287 | queue.splice(i, 1) |
288 | return msg |
289 | } |
290 | pull( |
291 | cat([ |
292 | dest[0] === '%' && self.app.pullGetMsg(dest), |
293 | self.app.sbot.links({ |
294 | rel: 'about', |
295 | dest: dest, |
296 | values: true, |
297 | private: true, |
298 | meta: false |
299 | }), |
300 | self.app.sbot.links({ |
301 | rel: 'root', |
302 | dest: dest, |
303 | values: true, |
304 | private: true, |
305 | meta: false |
306 | }) |
307 | ]), |
308 | pull.unique('key'), |
309 | self.app.unboxMessages(), |
310 | pull.drain(function (msg) { |
311 | var c = msg.value.content |
312 | if (!c) return |
313 | msg = { |
314 | key: msg.key, |
315 | ts: Math.min(now, |
316 | Number(msg.timestamp) || Infinity, |
317 | Number(msg.value.timestamp || c.timestamp) || Infinity), |
318 | value: msg.value, |
319 | branches: u.toLinkArray(c.branch).map(u.linkDest) |
320 | } |
321 | if (!msg.branches.length) { |
322 | enqueue(msg) |
323 | } else msg.branches.forEach(function (id) { |
324 | var linksToMsg = backlinks[id] || (backlinks[id] = []) |
325 | linksToMsg.push(msg) |
326 | }) |
327 | }, function (err) { |
328 | if (err) return cb(err) |
329 | while (queue.length) { |
330 | var msg = dequeue() |
331 | var abouts = msg.branches.map(function (id) { return aboutAtMsgs[id] }) |
332 | aboutAtMsgs[msg.key] = reduceAbout(abouts, premapAbout(msg)) |
333 | var linksToMsg = backlinks[msg.key] |
334 | if (linksToMsg) linksToMsg.forEach(enqueue) |
335 | } |
336 | var headAbouts = [] |
337 | var headIds = [] |
338 | for (var id in aboutAtMsgs) { |
339 | if (backlinks[id]) continue |
340 | headIds.push(id) |
341 | headAbouts.push(aboutAtMsgs[id]) |
342 | } |
343 | headAbouts.sort(compareByTs) |
344 | var about = postmapAbout(reduceAbout(headAbouts)) |
345 | about.branch = headIds |
346 | cb(null, about) |
347 | }) |
348 | ) |
349 | } |
350 | |
351 | function getValue(obj) { |
352 | return obj.value |
353 | } |
354 | |
355 | About.prototype.getSocial = function (dest, cb) { |
356 | var self = this |
357 | var myAbout = [] |
358 | var aboutByFeed = {} |
359 | var aboutByFeedFollowed = {} |
360 | if (!this.app.sbot.links) return cb(new Error('missing sbot.links')) |
361 | this.follows.getFollows(this.myId, function (err, follows) { |
362 | if (err) return cb(err) |
363 | pull( |
364 | cat([ |
365 | dest[0] === '%' && self.app.pullGetMsg(dest), |
366 | self.app.sbot.links({ |
367 | rel: 'about', |
368 | dest: dest, |
369 | values: true, |
370 | private: true, |
371 | meta: false |
372 | }) |
373 | ]), |
374 | self.app.unboxMessages(), |
375 | pull.drain(function (msg) { |
376 | var author = msg.value.author |
377 | var c = msg.value.content |
378 | if (!c) return |
379 | if (msg.key === dest && c.type === 'about') { |
380 | // don't describe an about message with itself |
381 | return |
382 | } |
383 | var about = author === self.myId ? myAbout : |
384 | follows[author] ? |
385 | aboutByFeedFollowed[author] || (aboutByFeedFollowed[author] = {}) : |
386 | aboutByFeed[author] || (aboutByFeed[author] = {}) |
387 | |
388 | if (c.name) about.name = c.name |
389 | if (c.title) about.title = c.title |
390 | if (c.image) { |
391 | about.image = u.linkDest(c.image) |
392 | about.imageLink = u.toLink(c.image) |
393 | } |
394 | if (c.description) about.description = c.description |
395 | if (c.publicWebHosting) about.publicWebHosting = c.publicWebHosting |
396 | }, function (err) { |
397 | if (err) return cb(err) |
398 | var destAbout = aboutByFeedFollowed[dest] || aboutByFeed[dest] |
399 | // bias the author's choices by giving them an extra vote |
400 | if (destAbout) { |
401 | if (follows[dest]) aboutByFeedFollowed._author = destAbout |
402 | else aboutByFeed._author = destAbout |
403 | } |
404 | var about = {} |
405 | var followedAbout = computeTopAbout(aboutByFeedFollowed) |
406 | var topAbout = computeTopAbout(aboutByFeed) |
407 | for (var k in topAbout) about[k] = topAbout[k] |
408 | // prefer followed feeds' choices |
409 | for (var k in followedAbout) about[k] = followedAbout[k] |
410 | // if we follow the destination/author feed, prefer its choices |
411 | if (follows[dest]) for (var k in destAbout) about[k] = destAbout[k] |
412 | // always prefer own choices |
413 | for (var k in myAbout) about[k] = myAbout[k] |
414 | cb(null, about) |
415 | }) |
416 | ) |
417 | }) |
418 | } |
419 |
Built with git-ssb-web