2015-09-23 05:44:28 +11:00
|
|
|
var pull = require('pull-stream')
|
|
|
|
var mlib = require('ssb-msgs')
|
|
|
|
var mime = require('mime-types')
|
|
|
|
var multicb = require('multicb')
|
|
|
|
var app = require('./app')
|
|
|
|
var social = require('./social-graph')
|
|
|
|
|
|
|
|
exports.debounce = function (fn, wait) {
|
|
|
|
var timeout
|
|
|
|
return function() {
|
|
|
|
clearTimeout(timeout)
|
|
|
|
timeout = setTimeout(fn, wait)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.getJson = function(path, cb) {
|
|
|
|
var xhr = new XMLHttpRequest()
|
|
|
|
xhr.open('GET', path, true)
|
|
|
|
xhr.responseType = 'json'
|
|
|
|
xhr.onreadystatechange = function() {
|
|
|
|
if (xhr.readyState == 4) {
|
|
|
|
var err
|
|
|
|
if (xhr.status < 200 || xhr.status >= 400)
|
|
|
|
err = new Error(xhr.status + ' ' + xhr.statusText)
|
|
|
|
cb(err, xhr.response)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
xhr.send()
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.postJson = function(path, obj, cb) {
|
|
|
|
var xhr = new XMLHttpRequest()
|
|
|
|
xhr.open('POST', path, true)
|
|
|
|
xhr.setRequestHeader('Content-Type', 'application/json')
|
|
|
|
xhr.responseType = 'json'
|
|
|
|
xhr.onreadystatechange = function() {
|
|
|
|
if (xhr.readyState == 4) {
|
|
|
|
var err
|
|
|
|
if (xhr.status < 200 || xhr.status >= 400)
|
|
|
|
err = new Error(xhr.status + ' ' + xhr.statusText)
|
|
|
|
cb(err, xhr.response)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
xhr.send(JSON.stringify(obj))
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.prettydate = require('nicedate')
|
|
|
|
|
|
|
|
var escapePlain =
|
|
|
|
exports.escapePlain = function(str) {
|
|
|
|
return (str||'')
|
|
|
|
.replace(/&/g, '&')
|
|
|
|
.replace(/</g, '<')
|
|
|
|
.replace(/>/g, '>')
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.shortString = function(str, len) {
|
|
|
|
len = len || 6
|
|
|
|
if (str.length - 3 > len)
|
|
|
|
return str.slice(0, len) + '...'
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
|
|
|
|
var dataSizes = ['kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb']
|
|
|
|
exports.bytesHuman = function (nBytes) {
|
|
|
|
var str = nBytes + 'b'
|
|
|
|
for (var i = 0, nApprox = nBytes / 1024; nApprox > 1; nApprox /= 1024, i++) {
|
|
|
|
str = nApprox.toFixed(2) + dataSizes[i]
|
|
|
|
}
|
|
|
|
return str
|
|
|
|
}
|
|
|
|
|
|
|
|
// http://stackoverflow.com/a/23329386
|
|
|
|
exports.stringByteLength = function (str) {
|
|
|
|
// returns the byte length of an utf8 string
|
|
|
|
var s = str.length;
|
|
|
|
for (var i=str.length-1; i>=0; i--) {
|
|
|
|
var code = str.charCodeAt(i);
|
|
|
|
if (code > 0x7f && code <= 0x7ff) s++;
|
|
|
|
else if (code > 0x7ff && code <= 0xffff) s+=2;
|
|
|
|
}
|
|
|
|
return s;
|
|
|
|
}
|
|
|
|
|
|
|
|
// https://stackoverflow.com/questions/2541481/get-average-color-of-image-via-javascript
|
|
|
|
exports.getAverageRGB = function (imgEl) {
|
|
|
|
|
|
|
|
var blockSize = 5, // only visit every 5 pixels
|
|
|
|
canvas = document.createElement('canvas'),
|
|
|
|
context = canvas.getContext && canvas.getContext('2d'),
|
|
|
|
data, width, height,
|
|
|
|
i = -4,
|
|
|
|
length,
|
|
|
|
rgb = {r:0,g:0,b:0},
|
|
|
|
count = 0
|
|
|
|
|
|
|
|
if (!context) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
height = canvas.height = imgEl.naturalHeight || imgEl.offsetHeight || imgEl.height
|
|
|
|
width = canvas.width = imgEl.naturalWidth || imgEl.offsetWidth || imgEl.width
|
|
|
|
|
|
|
|
context.drawImage(imgEl, 0, 0)
|
|
|
|
|
|
|
|
try {
|
|
|
|
data = context.getImageData(0, 0, width, height)
|
|
|
|
} catch(e) {
|
|
|
|
return null
|
|
|
|
}
|
|
|
|
|
|
|
|
length = data.data.length
|
|
|
|
while ( (i += blockSize * 4) < length ) {
|
|
|
|
++count
|
|
|
|
rgb.r += data.data[i]
|
|
|
|
rgb.g += data.data[i+1]
|
|
|
|
rgb.b += data.data[i+2]
|
|
|
|
}
|
|
|
|
|
|
|
|
rgb.r = (rgb.r/count)|0
|
|
|
|
rgb.g = (rgb.g/count)|0
|
|
|
|
rgb.b = (rgb.b/count)|0
|
|
|
|
|
|
|
|
return rgb
|
|
|
|
}
|
|
|
|
|
|
|
|
function votesFetcher (fetchFn) {
|
|
|
|
return function (voteTopic, cb) {
|
|
|
|
var stats = { uservote: 0, voteTally: 0, votes: {}, upvoters: [], downvoters: [] }
|
|
|
|
pull(
|
|
|
|
app.ssb[fetchFn]({ id: voteTopic, rel: 'voteTopic' }),
|
|
|
|
pull.asyncMap(function (link, cb2) { app.ssb.get(link.message, cb2) }),
|
|
|
|
pull.collect(function (err, voteMsgs) {
|
|
|
|
if (err)
|
|
|
|
return cb(err)
|
|
|
|
// collect final votes
|
|
|
|
voteMsgs.forEach(function (m) {
|
|
|
|
if (m.content.type !== 'vote')
|
|
|
|
return
|
|
|
|
if (m.content.vote === 1 || m.content.vote === 0 || m.content.vote === -1)
|
|
|
|
stats.votes[m.author] = m.content.vote
|
|
|
|
})
|
|
|
|
// tally the votes
|
|
|
|
for (var author in stats.votes) {
|
|
|
|
var v = stats.votes[author]
|
|
|
|
if (v === 1) {
|
|
|
|
stats.upvoters.push(author)
|
|
|
|
stats.voteTally++
|
|
|
|
}
|
|
|
|
else if (v === -1) {
|
|
|
|
stats.downvoters.push(author)
|
|
|
|
stats.voteTally--
|
|
|
|
}
|
|
|
|
}
|
|
|
|
stats.uservote = stats.votes[app.user.id] || 0
|
|
|
|
cb(null, stats)
|
|
|
|
})
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// :TODO: cant do fetchMsgVotes because the messagesLinkedToMessage fetcher works differently than all the others
|
|
|
|
// see https://github.com/ssbc/secure-scuttlebutt/issues/99
|
|
|
|
exports.fetchFeedVotes = votesFetcher('messagesLinkedToFeed')
|
|
|
|
exports.fetchExtVotes = votesFetcher('feedsLinkedToExternal')
|
|
|
|
|
|
|
|
exports.calcMessageStats = function (thread, opts) {
|
|
|
|
var stats = { comments: 0, uservote: 0, voteTally: 0, votes: {} }
|
|
|
|
|
|
|
|
function process (t, depth) {
|
|
|
|
if (!t.related)
|
|
|
|
return
|
|
|
|
|
|
|
|
t.related.forEach(function (r) {
|
|
|
|
var c = r.value.content
|
|
|
|
|
|
|
|
// only process votes for immediate children
|
|
|
|
if (depth === 0 && c.type === 'vote') {
|
|
|
|
// track latest choice, dont tally yet in case multiple votes by one user
|
|
|
|
stats.votes[r.value.author] = c.vote
|
|
|
|
}
|
|
|
|
else if (c.type !== 'vote') {
|
|
|
|
// count non-votes as a comment
|
|
|
|
stats.comments++
|
|
|
|
}
|
|
|
|
|
|
|
|
// recurse
|
|
|
|
if (opts && opts.recursive)
|
|
|
|
process(r)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
process(thread, 0)
|
|
|
|
|
|
|
|
// now tally the votes
|
|
|
|
for (var author in stats.votes) {
|
|
|
|
var v = stats.votes[author]
|
|
|
|
if (v === 1) {
|
|
|
|
stats.voteTally++
|
|
|
|
}
|
|
|
|
else if (v === -1) {
|
|
|
|
stats.voteTally--
|
|
|
|
}
|
|
|
|
}
|
|
|
|
stats.uservote = stats.votes[app.user.id] || 0
|
|
|
|
|
|
|
|
return stats
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.getOtherNames = function (profile) {
|
|
|
|
// todo - replace with ranked names
|
|
|
|
var name = app.users.names[profile.id] || profile.id
|
|
|
|
|
|
|
|
var names = []
|
|
|
|
function add(n) {
|
|
|
|
if (n && n !== name && !~names.indexOf(n))
|
|
|
|
names.push(n)
|
|
|
|
}
|
|
|
|
|
|
|
|
// get 3 of the given or self-assigned names
|
|
|
|
add(profile.self.name)
|
|
|
|
for (var k in profile.assignedBy) {
|
|
|
|
if (names.length >= 3)
|
|
|
|
break
|
|
|
|
add(profile.assignedBy[k].name)
|
|
|
|
}
|
|
|
|
return names
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.getParentThread = function (mid, cb) {
|
|
|
|
up()
|
|
|
|
function up () {
|
|
|
|
app.ssb.get(mid, function (err, msg) {
|
|
|
|
if (err)
|
|
|
|
return cb(err)
|
|
|
|
|
|
|
|
// not found? finish here
|
|
|
|
if (!msg)
|
|
|
|
return finish()
|
|
|
|
|
|
|
|
// root link? go straight to that
|
|
|
|
if (mlib.link(msg.content.root, 'msg')) {
|
|
|
|
mid = mlib.link(msg.content.root).link
|
|
|
|
return finish()
|
|
|
|
}
|
|
|
|
|
|
|
|
// branch link? ascend
|
|
|
|
if (mlib.link(msg.content.branch, 'msg')) {
|
|
|
|
mid = mlib.link(msg.content.branch).link
|
|
|
|
return up()
|
|
|
|
}
|
|
|
|
|
|
|
|
// topmost, finish
|
|
|
|
finish()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
function finish () {
|
|
|
|
app.ssb.relatedMessages({ id: mid, count: true, parent: true }, cb)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.decryptThread = function (thread, cb) {
|
|
|
|
var done = multicb()
|
|
|
|
if (thread.related)
|
|
|
|
iterate(thread.related)
|
|
|
|
done(cb)
|
|
|
|
|
|
|
|
function iterate (msgs) {
|
|
|
|
msgs.forEach(function (msg) {
|
|
|
|
msg.plaintext = (typeof msg.value.content != 'string')
|
|
|
|
if (!msg.plaintext)
|
|
|
|
decrypt(msg)
|
|
|
|
if (msg.related)
|
|
|
|
iterate(msg.related)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
function decrypt (msg) {
|
|
|
|
var cb2 = done()
|
|
|
|
app.ssb.private.unbox(msg.value.content, function (err, decrypted) {
|
|
|
|
if (decrypted)
|
|
|
|
msg.value.content = decrypted
|
|
|
|
cb2()
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
exports.getPubStats = function () {
|
2015-09-30 02:58:23 +11:00
|
|
|
var membersof=0, active=0, untried=0
|
2015-09-23 05:44:28 +11:00
|
|
|
app.peers.forEach(function (peer) {
|
|
|
|
// filter out LAN peers
|
|
|
|
if (peer.host == 'localhost' || peer.host.indexOf('192.168.') === 0)
|
|
|
|
return
|
|
|
|
if (social.follows(peer.key, app.user.id)) {
|
|
|
|
membersof++
|
|
|
|
if (peer.time && peer.time.connect && (peer.time.connect > peer.time.attempt) || peer.connected)
|
|
|
|
active++
|
2015-09-30 02:58:23 +11:00
|
|
|
if (!peer.time || !peer.time.attempt)
|
|
|
|
untried++
|
2015-09-23 05:44:28 +11:00
|
|
|
}
|
|
|
|
})
|
2015-09-30 02:58:23 +11:00
|
|
|
|
|
|
|
return { membersof: membersof, active: active, untried: untried, hasSyncIssue: (!membersof || (!untried && !active)) }
|
2015-09-23 05:44:28 +11:00
|
|
|
}
|
|
|
|
|
|
|
|
exports.getExtLinkName = function (link) {
|
|
|
|
if (link.name && typeof link.name == 'string')
|
|
|
|
return link.name
|
|
|
|
if (link.type) {
|
|
|
|
var ext = mime.extension(link.type)
|
|
|
|
if (ext)
|
|
|
|
return 'untitled.'+ext
|
|
|
|
}
|
|
|
|
return 'untitled'
|
|
|
|
}
|