var pull = require('pull-stream') var multicb = require('multicb') var cat = require('pull-cat') var u = require('./util') module.exports = About function About(app, myId, follows) { this.app = app this.myId = myId this.follows = follows } About.prototype.createAboutOpStream = function (id) { return pull( this.app.sbot.links({dest: id, rel: 'about', values: true, reverse: true, private: true, meta: false}), this.app.unboxMessages(), pull.map(function (msg) { var c = msg.value.content if (typeof c !== 'object' || c === null) return [] return Object.keys(c).filter(function (key) { return key !== 'about' && key !== 'type' && key !== 'recps' && key !== 'reason' }).map(function (key) { var value = c[key] return { id: msg.key, author: msg.value.author, timestamp: msg.value.timestamp, prop: key, value: value, remove: value && value.remove, } }) }), pull.flatten() ) } About.prototype.createAboutStreams = function (id) { var ops = this.createAboutOpStream(id) var scalars = {/* author: {prop: value} */} var sets = {/* author: {prop: {link}} */} var setsDone = multicb({pluck: 1, spread: true}) setsDone()(null, pull.values([])) return { scalars: pull( ops, pull.unique(function (op) { return op.author + '-' + op.prop + '-' }), pull.filter(function (op) { return !op.remove }) ), sets: u.readNext(setsDone) } } function computeTopAbout(aboutByFeed) { var propValueCounts = {/* prop: {value: count} */} var topValues = {/* prop: value */} var topValueCounts = {/* prop: count */} for (var feed in aboutByFeed) { var feedAbout = aboutByFeed[feed] for (var prop in feedAbout) { var value = feedAbout[prop] var valueCounts = propValueCounts[prop] || (propValueCounts[prop] = {}) var count = (valueCounts[value] || 0) + 1 valueCounts[value] = count if (count > (topValueCounts[prop] || 0)) { topValueCounts[prop] = count topValues[prop] = value } } } return topValues } About.prototype.get = function (dest, cb) { if (dest[0] === '@') return this.getSocial(dest, cb) return this.getCausal(dest, cb) } function copy(obj) { if (obj === null || typeof obj !== 'object') return obj var o = {} for (var k in obj) o[k] = obj[k] return o } function premapAbout(msg) { var value = { about: {}, mentions: {}, branches: {}, sources: {}, ts: msg.ts } var c = msg.value.content if (!c) return value if (c.branch) { msg.branches.map(function (id) { value.branches[id] = true }) } if (!c.about && c.root) return value value.about = {} var author = msg.value.author var source = {id: msg.key, seq: msg.value.sequence} for (var k in c) switch(k) { case 'type': case 'about': case 'branch': break case 'recps': // get recps from root message only if (!c.root && !c.about) { value.recps = {} u.toLinkArray(c.recps).forEach(function (link) { value.recps[link.link] = link }) } break case 'mentions': value.mentions = {} u.toLinkArray(c.mentions).map(function (link) { value.mentions[link.link] = link }) break case 'image': var link = u.toLink(c.image) if (!link) break value.about.image = link.link value.about.imageLink = link var sources = value.sources.image || (value.sources.image = []) sources[author] = source break case 'attendee': var attendee = u.linkDest(c.attendee) if (attendee && attendee === author) { // TODO: allow users adding other users as attendees? var attendeeLink = copy(u.toLink(c.attendee)) attendeeLink.source = msg.key value.attending = {} value.attending[attendee] = attendeeLink.remove ? null : attendeeLink } break default: // TODO: handle arrays value.about[k] = c[k] var sources = value.sources[k] || (value.sources[k] = {}) sources[author] = source } return value } function reduceAbout(values, lastValue) { var newValue = { about: {}, mentions: {}, branches: {}, sources: {} } values.sort(compareByTs).concat(lastValue).forEach(function (value) { if (!value) return if (value.ts) { if (!newValue.ts || value.ts > newValue.ts) { newValue.ts = value.ts } } if (value.attending) { var attending = newValue.attending || (newValue.attending = {}) for (var k in value.attending) { attending[k] = value.attending[k] } } if (value.mentions) for (var k in value.mentions) { newValue.mentions[k] = value.mentions[k] } if (value.branches) for (var k in value.branches) { newValue.branches[k] = value.branches[k] } if (value.recps) { // note: zero recps is still private. truthy recps indicates private var recps = newValue.recps || (newValue.recps = {}) for (var k in value.recps) { newValue.recps[k] = value.recps[k] } } if (value.about) for (var k in value.about) { // TODO: use merge heuristics newValue.about[k] = value.about[k] if (lastValue && lastValue.about[k]) { if (value === lastValue) { newValue.sources[k] = lastValue.sources[k] } else { // message setting a property resets the property's sources from branches } } else { var newSources = newValue.sources[k] || (newValue.sources[k] = {}) var sources = value.sources[k] for (var feed in sources) { if (newSources[feed] && newSources[feed].seq > sources[feed].seq) { // assume causal order in user's own feed. // this condition shouldn't be reached if messages are in feed order console.error('skip', k, feed, sources[feed].id, newSources[feed].id) continue } newSources[feed] = sources[feed] } } } }) return newValue } function postmapAbout(value) { var about = { ts: value.ts, _sources: {} } for (var k in value.sources) { var propSources = about._sources[k] = [] for (var feed in value.sources[k]) { propSources.push(value.sources[k][feed].id) } } if (value.mentions) { about.mentions = [] for (var k in value.mentions) { about.mentions.push(value.mentions[k]) } } if (value.attending) { about.attendee = [] for (var k in value.attending) { var link = value.attending[k] if (link) about.attendee.push(link) } } if (value.branches) about.branch = Object.keys(value.branches) if (value.recps) { about.recps = [] for (var k in value.recps) { about.recps.push(value.recps[k]) } } if (value.about) for (var k in value.about) { about[k] = value.about[k] } return about } function compareByTs(a, b) { return a.ts - b.ts } About.prototype.getCausal = function (dest, cb) { var self = this var backlinks = {} var seen = {} var queue = [] var aboutAtMsgs = {} var now = Date.now() function enqueue(msg) { if (!seen[msg.key]) { seen[msg.key] = true queue.push(msg) } } function isMsgIdDone(id) { return !!aboutAtMsgs[id] } function isMsgReady(msg) { return msg.branches.every(isMsgIdDone) } function dequeue() { var msg = queue.filter(isMsgReady).sort(compareByTs)[0] if (!msg) return console.error('thread error'), queue.shift() var i = queue.indexOf(msg) queue.splice(i, 1) return msg } pull( cat([ dest[0] === '%' && self.app.pullGetMsg(dest), self.app.sbot.links({ rel: 'about', dest: dest, values: true, private: true, meta: false }), self.app.sbot.links({ rel: 'root', dest: dest, values: true, private: true, meta: false }) ]), pull.unique('key'), self.app.unboxMessages(), pull.drain(function (msg) { var c = msg.value.content if (!c) return msg = { key: msg.key, ts: Math.min(now, Number(msg.timestamp) || Infinity, Number(msg.value.timestamp || c.timestamp) || Infinity), value: msg.value, branches: u.toLinkArray(c.branch).map(u.linkDest) } if (!msg.branches.length) { enqueue(msg) } else msg.branches.forEach(function (id) { var linksToMsg = backlinks[id] || (backlinks[id] = []) linksToMsg.push(msg) }) }, function (err) { if (err) return cb(err) while (queue.length) { var msg = dequeue() var abouts = msg.branches.map(function (id) { return aboutAtMsgs[id] }) aboutAtMsgs[msg.key] = reduceAbout(abouts, premapAbout(msg)) var linksToMsg = backlinks[msg.key] if (linksToMsg) linksToMsg.forEach(enqueue) } var headAbouts = [] var headIds = [] for (var id in aboutAtMsgs) { if (backlinks[id]) continue headIds.push(id) headAbouts.push(aboutAtMsgs[id]) } headAbouts.sort(compareByTs) var about = postmapAbout(reduceAbout(headAbouts)) about.branch = headIds cb(null, about) }) ) } function getValue(obj) { return obj.value } About.prototype.getSocial = function (dest, cb) { var self = this var myAbout = [] var aboutByFeed = {} var aboutByFeedFollowed = {} this.follows.getFollows(this.myId, function (err, follows) { if (err) return cb(err) pull( cat([ dest[0] === '%' && self.app.pullGetMsg(dest), self.app.sbot.links({ rel: 'about', dest: dest, values: true, private: true, meta: false }) ]), self.app.unboxMessages(), pull.drain(function (msg) { var author = msg.value.author var c = msg.value.content if (!c) return if (msg.key === dest && c.type === 'about') { // don't describe an about message with itself return } var about = author === self.myId ? myAbout : follows[author] ? aboutByFeedFollowed[author] || (aboutByFeedFollowed[author] = {}) : aboutByFeed[author] || (aboutByFeed[author] = {}) if (c.name) about.name = c.name if (c.title) about.title = c.title if (c.image) { about.image = u.linkDest(c.image) about.imageLink = u.toLink(c.image) } if (c.description) about.description = c.description if (c.publicWebHosting) about.publicWebHosting = c.publicWebHosting }, function (err) { if (err) return cb(err) var destAbout = aboutByFeedFollowed[dest] || aboutByFeed[dest] // bias the author's choices by giving them an extra vote if (destAbout) { if (follows[dest]) aboutByFeedFollowed._author = destAbout else aboutByFeed._author = destAbout } var about = {} var followedAbout = computeTopAbout(aboutByFeedFollowed) var topAbout = computeTopAbout(aboutByFeed) for (var k in topAbout) about[k] = topAbout[k] // prefer followed feeds' choices for (var k in followedAbout) about[k] = followedAbout[k] // if we follow the destination/author feed, prefer its choices if (follows[dest]) for (var k in destAbout) about[k] = destAbout[k] // always prefer own choices for (var k in myAbout) about[k] = myAbout[k] cb(null, about) }) ) }) }