git ssb

16+

cel / patchfoo



Tree: d185716fb11842271c37ff499725b2b66ba2a5e3

Files: d185716fb11842271c37ff499725b2b66ba2a5e3 / lib / about.js

11931 bytesRaw
1var pull = require('pull-stream')
2var multicb = require('multicb')
3var cat = require('pull-cat')
4var u = require('./util')
5
6module.exports = About
7
8function About(app, myId, follows) {
9 this.app = app
10 this.myId = myId
11 this.follows = follows
12}
13
14About.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
42About.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
63function 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
83About.prototype.get = function (dest, cb) {
84 if (dest[0] === '@') return this.getSocial(dest, cb)
85 return this.getCausal(dest, cb)
86}
87
88function 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
95function 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
162function 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
222function 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
259function compareByTs(a, b) {
260 return a.ts - b.ts
261}
262
263About.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
351function getValue(obj) {
352 return obj.value
353}
354
355About.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