ferment
Version:
Peer-to-peer audio publishing and streaming application. Like SoundCloud but decentralized. A mashup of ssb, webtorrent and electron.
228 lines (207 loc) • 7.54 kB
JavaScript
var MutantDict = require('@mmckegg/mutant/dict')
var pull = require('pull-stream')
var Profile = require('../models/profile')
var MutantLookup = require('@mmckegg/mutant/lookup')
var toCollection = require('@mmckegg/mutant/dict-to-collection')
var mlib = require('ssb-msgs')
var computed = require('@mmckegg/mutant/computed')
var MutantMap = require('@mmckegg/mutant/map')
var MutantArray = require('@mmckegg/mutant/array')
var MutantSet = require('@mmckegg/mutant/set')
var Value = require('@mmckegg/mutant/value')
var concat = require('@mmckegg/mutant/concat')
var AudioPost = require('../models/audio-post')
var throttle = require('@mmckegg/mutant/throttle')
var extend = require('xtend')
var ip = require('ip')
var ObservLocal = require('../lib/obs-local')
module.exports = function (ssbClient, config) {
var pubIds = MutantSet()
var lookup = MutantDict()
var postLookup = MutantDict()
var postIds = MutantArray()
var profileIds = MutantArray()
var localPeerIds = ObservLocal(ssbClient, config)
var profilesList = toCollection(lookup)
var lookupByName = MutantLookup(profilesList, 'displayName')
var sync = Value(false)
var pubFriends = concat(MutantMap(pubIds, (id) => get(id).following))
var pubFriendPostIds = computed([concat([pubFriends, localPeerIds]), postIds], (pubFriends, postIds) => {
return postIds.filter((id) => {
var authorId = postLookup.get(id).author.id
return pubFriends.includes(authorId) || authorId === ssbClient.id
})
})
pollPeers()
setInterval(pollPeers, 5000)
pull(
ssbClient.createFeedStream({ live: true }),
pull.drain((data) => {
if (data.sync) {
sync.set(true)
} else if (data.value.content.type === 'about') {
mlib.links(data.value.content.about, 'feed').forEach(function (link) {
const profile = get(link.link)
profile.updateFrom(data.value.author, data)
})
} else if (data.value.content.type === 'contact') {
const following = data.value.content.following
const author = get(data.value.author)
mlib.links(data.value.content.contact, 'feed').forEach(function (link) {
if (typeof following === 'boolean') {
if (following) {
author.following.add(link.link)
var target = get(link.link)
target.followers.add(data.value.author)
if (typeof data.value.content.scope === 'string') {
target.scopes.add(data.value.content.scope)
}
} else {
author.following.delete(link.link)
const target = lookup.get(link.link)
if (target) {
target.followers.delete(data.value.author)
}
}
}
})
} else if (data.value.content.type === 'pub') {
if (data.value.author === ssbClient.id) {
if (data.value.content.address && data.value.content.address.key) {
const id = mlib.link(data.value.content.address.key, 'feed')
if (id) pubIds.add(id.link)
}
}
} else if (data.value.content.type === 'ferment/audio') {
const profile = get(data.value.author)
const post = getPost(data.key)
post.set(extend(data.value.content, { timestamp: data.value.timestamp }))
post.author = profile
profile.posts.add(data.key)
postIds.push(data.key)
} else if (data.value.content.type === 'ferment/update') {
const update = mlib.link(data.value.content.update, 'msg')
const post = postLookup.get(update.link)
if (post) {
post.updateFrom(data)
}
} else if (data.value.content.type === 'ferment/like') {
const profile = get(data.value.author)
const like = mlib.link(data.value.content.like, 'msg')
const post = postLookup.get(like.link)
if (like.value) {
profile.likes.add(like.link)
if (post) post.likes.add(data.value.author)
} else {
profile.likes.delete(like.link)
if (post) post.likes.delete(data.value.author)
}
} else if (data.value.content.type === 'ferment/repost') {
const profile = get(data.value.author)
const repost = mlib.link(data.value.content.repost, 'msg')
const post = postLookup.get(repost.link)
if (repost.value) {
profile.posts.add(repost.link)
profile.reposts.put(repost.link, data.value.timestamp)
if (post) post.reposters.add(data.value.author)
} else {
profile.posts.delete(repost.link)
profile.reposts.delete(repost.link)
if (post) post.reposters.delete(data.value.author)
}
}
})
)
return {
get,
getPost,
getSuggested,
pubFriendPostIds,
rankProfileIds,
lookup,
lookupByName,
postIds,
pubIds,
postLookup,
sync
}
function pollPeers () {
ssbClient.gossip.peers((err, values) => {
if (err) console.log(err)
values.forEach((peer) => {
if (!ip.isPrivate(peer.host)) {
var profile = get(peer.key)
if (!profile.isPub()) {
profile.isPub.set(true)
}
}
})
})
}
function get (id) {
if (id.id) {
// already a profile?
return id
}
var profile = lookup.get(id)
if (!profile) {
profile = Profile(id, ssbClient.id)
lookup.put(id, profile)
profileIds.push(id)
}
return profile
}
function getPost (id) {
if (id.id) {
return id
}
var instance = postLookup.get(id)
if (!instance) {
instance = AudioPost(id, ssbClient.id)
postLookup.put(id, instance)
}
return instance
}
function rankProfileIds (ids, max) {
return computed(ids, (ids) => {
var result = []
ids.forEach((id) => {
var profile = get(id)
var displayNameBonus = profile.displayNames.keys().length ? 10 : 0
var imageBonus = profile.images.keys().length ? 2 : 0
var postBonus = Math.log(1 + profile.postCount())
var followerBonus = postBonus ? Math.log(1 + profile.followers.getLength()) : Math.log(profile.followers.getLength() / 100)
result.push([id, postBonus + followerBonus + displayNameBonus + imageBonus])
})
result = result.sort((a, b) => b[1] - a[1]).map(x => x[0])
if (max) {
result = result.slice(0, max)
}
return result
})
}
function getSuggested (max) {
var yourProfile = get(ssbClient.id)
var ids = computed([sync, throttle(profileIds, 2000), throttle(pubFriends, 2000)], (sync, ids) => {
if (sync) {
var result = []
ids.forEach((id) => {
var profile = get(id)
if ((!config.friends.scope || profile.scopes.has(config.friends.scope)) && !yourProfile.following.has(id) && id !== yourProfile.id && profile.displayNames.keys().length && !profile.isPub()) {
var postBonus = Math.log(1 + profile.postCount())
var followerBonus = postBonus ? Math.log(1 + profile.followers.getLength()) : Math.log(profile.followers.getLength() / 100)
result.push([id, postBonus + followerBonus])
}
})
result = result.sort((a, b) => b[1] - a[1])
if (max) {
result = result.slice(0, max)
}
return result.map(x => x[0])
}
}, { nextTick: true })
var result = MutantMap(ids, get)
result.sync = sync
return result
}
}