370 lines
11 KiB
JavaScript
370 lines
11 KiB
JavaScript
|
var mlib = require('ssb-msgs')
|
||
|
var u = require('./util')
|
||
|
|
||
|
module.exports = function (sbot, db, state, emit) {
|
||
|
|
||
|
var processors = {
|
||
|
post: function (msg) {
|
||
|
var me = getProfile(sbot.id)
|
||
|
var author = msg.value.author
|
||
|
var by_me = (author === sbot.id)
|
||
|
var c = msg.value.content
|
||
|
|
||
|
// home index
|
||
|
if (c.recps)
|
||
|
return // skip targeted messages
|
||
|
|
||
|
state.home.sortedUpsert(msg.value.timestamp, msg.key)
|
||
|
if (mlib.link(c.root, 'msg')) {
|
||
|
// a reply, put its *parent* in the home index
|
||
|
state.pinc()
|
||
|
u.getRootMsg(sbot, msg, function (err, rootmsg) {
|
||
|
if (rootmsg && typeof rootmsg.value.content != 'string') // dont put encrypted msgs in homestream
|
||
|
state.home.sortedUpsert(rootmsg.value.timestamp, rootmsg.key)
|
||
|
state.pdec()
|
||
|
})
|
||
|
}
|
||
|
|
||
|
if (!by_me) {
|
||
|
// emit home-add if by a followed user and in the last hour
|
||
|
if (follows(sbot.id, author) && ((Date.now() - msg.value.timestamp) < 1000*60*60))
|
||
|
emit('home-add')
|
||
|
}
|
||
|
|
||
|
// inbox index
|
||
|
if (!by_me) {
|
||
|
var inboxed = false
|
||
|
mlib.links(c.root, 'msg').forEach(function (link) {
|
||
|
if (inboxed) return
|
||
|
// a reply to my messages?
|
||
|
if (state.mymsgs.indexOf(link.link) >= 0) {
|
||
|
var row = state.inbox.sortedInsert(msg.value.timestamp, msg.key)
|
||
|
attachIsRead(row)
|
||
|
row.author = msg.value.author // inbox index is filtered on read by the friends graph
|
||
|
if (follows(sbot.id, row.author))
|
||
|
emit('index-change', { index: 'inbox' })
|
||
|
inboxed = true
|
||
|
}
|
||
|
})
|
||
|
mlib.links(c.mentions, 'feed').forEach(function (link) {
|
||
|
if (inboxed) return
|
||
|
// mentions me?
|
||
|
if (link.link == sbot.id) {
|
||
|
var row = state.inbox.sortedInsert(msg.value.timestamp, msg.key)
|
||
|
attachIsRead(row)
|
||
|
row.author = msg.value.author // inbox index is filtered on read by the friends graph
|
||
|
if (follows(sbot.id, row.author))
|
||
|
emit('index-change', { index: 'inbox' })
|
||
|
inboxed = true
|
||
|
}
|
||
|
})
|
||
|
}
|
||
|
},
|
||
|
|
||
|
site: function (msg) {
|
||
|
var site = getSite(msg.value.author)
|
||
|
|
||
|
// additions
|
||
|
mlib.links(msg.value.content.includes, 'blob').forEach(function (link) {
|
||
|
if (!link.path)
|
||
|
return
|
||
|
site[link.path] = link
|
||
|
})
|
||
|
|
||
|
// removals
|
||
|
var excludes = msg.value.content.excludes
|
||
|
if (excludes) {
|
||
|
;(Array.isArray(excludes) ? excludes : [excludes]).forEach(function (item) {
|
||
|
if (!item.path)
|
||
|
return
|
||
|
delete site[item.path]
|
||
|
})
|
||
|
}
|
||
|
},
|
||
|
|
||
|
contact: function (msg) {
|
||
|
// update profiles
|
||
|
mlib.links(msg.value.content.contact, 'feed').forEach(function (link) {
|
||
|
var toself = link.link === msg.value.author
|
||
|
if (toself) updateSelfContact(msg.value.author, msg)
|
||
|
else updateOtherContact(msg.value.author, link.link, msg)
|
||
|
})
|
||
|
},
|
||
|
|
||
|
about: function (msg) {
|
||
|
// update profiles
|
||
|
mlib.links(msg.value.content.about, 'feed').forEach(function (link) {
|
||
|
var toself = link.link === msg.value.author
|
||
|
if (toself) updateSelfContact(msg.value.author, msg)
|
||
|
else updateOtherContact(msg.value.author, link.link, msg)
|
||
|
})
|
||
|
},
|
||
|
|
||
|
vote: function (msg) {
|
||
|
// update tallies
|
||
|
var link = mlib.link(msg.value.content.vote, 'msg')
|
||
|
|
||
|
if (link) {
|
||
|
if (msg.value.author == sbot.id)
|
||
|
updateMyVote(msg, link)
|
||
|
else if (state.mymsgs.indexOf(link.link) >= 0) // vote on my msg?
|
||
|
updateVoteOnMymsg(msg, link)
|
||
|
}
|
||
|
},
|
||
|
|
||
|
flag: function (msg) {
|
||
|
// inbox index
|
||
|
var link = mlib.link(msg.value.content.flag, 'msg')
|
||
|
if (sbot.id != msg.value.author && link && state.mymsgs.indexOf(link.link) >= 0) {
|
||
|
var row = state.inbox.sortedInsert(msg.value.timestamp, msg.key)
|
||
|
attachIsRead(row)
|
||
|
row.author = msg.value.author // inbox index is filtered on read by the friends graph
|
||
|
if (follows(sbot.id, msg.value.author))
|
||
|
emit('index-change', { index: 'inbox' })
|
||
|
}
|
||
|
|
||
|
// user flags
|
||
|
var link = mlib.link(msg.value.content.flag, 'feed')
|
||
|
if (link) {
|
||
|
var source = getProfile(msg.value.author)
|
||
|
var target = getProfile(link.link)
|
||
|
|
||
|
var flag = link.reason ? { key: msg.key, reason: link.reason } : false
|
||
|
source.assignedTo[target.id].flagged = flag
|
||
|
target.assignedBy[source.id].flagged = flag
|
||
|
|
||
|
// track if by local user
|
||
|
if (source.id === sbot.id)
|
||
|
target.flagged = flag
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function getProfile (pid) {
|
||
|
if (pid.id) // already a profile?
|
||
|
return pid
|
||
|
|
||
|
var profile = state.profiles[pid]
|
||
|
if (!profile) {
|
||
|
state.profiles[pid] = profile = {
|
||
|
id: pid,
|
||
|
|
||
|
// current values...
|
||
|
self: { name: null, image: null }, // ...set by self about self
|
||
|
assignedBy: {}, // ...set by others about self
|
||
|
assignedTo: {}, // ...set by self about others
|
||
|
|
||
|
// has local user flagged?
|
||
|
flagged: false
|
||
|
}
|
||
|
}
|
||
|
return profile
|
||
|
}
|
||
|
|
||
|
function getSite (pid) {
|
||
|
var site = state.sites[pid]
|
||
|
if (!site)
|
||
|
state.sites[pid] = site = {}
|
||
|
return site
|
||
|
}
|
||
|
|
||
|
function updateSelfContact (author, msg) {
|
||
|
var c = msg.value.content
|
||
|
author = getProfile(author)
|
||
|
|
||
|
// name: a non-empty string
|
||
|
if (nonEmptyStr(c.name)) {
|
||
|
author.self.name = makeNameSafe(c.name)
|
||
|
rebuildNamesFor(author)
|
||
|
}
|
||
|
|
||
|
// image: link to image
|
||
|
if ('image' in c) {
|
||
|
if (mlib.link(c.image, 'blob'))
|
||
|
author.self.image = mlib.link(c.image)
|
||
|
else if (!c.image)
|
||
|
delete author.self.image
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function updateOtherContact (source, target, msg) {
|
||
|
var c = msg.value.content
|
||
|
source = getProfile(source)
|
||
|
target = getProfile(target)
|
||
|
source.assignedTo[target.id] = source.assignedTo[target.id] || {}
|
||
|
target.assignedBy[source.id] = target.assignedBy[source.id] || {}
|
||
|
var userProf = getProfile(sbot.id)
|
||
|
|
||
|
// name: a non-empty string
|
||
|
if (nonEmptyStr(c.name)) {
|
||
|
source.assignedTo[target.id].name = makeNameSafe(c.name)
|
||
|
target.assignedBy[source.id].name = makeNameSafe(c.name)
|
||
|
rebuildNamesFor(target)
|
||
|
}
|
||
|
|
||
|
// following: bool
|
||
|
if (typeof c.following === 'boolean') {
|
||
|
source.assignedTo[target.id].following = c.following
|
||
|
target.assignedBy[source.id].following = c.following
|
||
|
|
||
|
// if from the user, update names (in case un/following changes conflict status)
|
||
|
if (source.id == sbot.id)
|
||
|
rebuildNamesFor(target)
|
||
|
|
||
|
// follows index
|
||
|
if (target.id == sbot.id) {
|
||
|
// use the follower's id as the key to this index, so we only have 1 entry per other user max
|
||
|
var row = state.follows.sortedUpsert(msg.value.timestamp, msg.key)
|
||
|
row.following = c.following
|
||
|
attachIsRead(row, msg.key)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// blocking: bool
|
||
|
if (typeof c.blocking === 'boolean') {
|
||
|
source.assignedTo[target.id].blocking = c.blocking
|
||
|
target.assignedBy[source.id].blocking = c.blocking
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function rebuildNamesFor (profile) {
|
||
|
profile = getProfile(profile)
|
||
|
|
||
|
// remove oldname from id->name map
|
||
|
var oldname = state.names[profile.id]
|
||
|
if (oldname) {
|
||
|
if (state.ids[oldname] == profile.id) {
|
||
|
// remove
|
||
|
delete state.ids[oldname]
|
||
|
} else if (Array.isArray(state.ids[oldname])) {
|
||
|
// is in a conflict, remove from conflict array
|
||
|
var i = state.ids[oldname].indexOf(profile.id)
|
||
|
if (i !== -1) {
|
||
|
state.ids[oldname].splice(i, 1)
|
||
|
if (state.ids[oldname].length === 1) {
|
||
|
// conflict resolved
|
||
|
delete state.actionItems[oldname]
|
||
|
state.ids[oldname] = state.ids[oldname][0]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// default to self-assigned name
|
||
|
var name = profile.self.name
|
||
|
if (profile.id !== sbot.id && profile.assignedBy[sbot.id] && profile.assignedBy[sbot.id].name) {
|
||
|
// use name assigned by the local user, if one is given
|
||
|
name = profile.assignedBy[sbot.id].name
|
||
|
}
|
||
|
if (!name)
|
||
|
return
|
||
|
|
||
|
// store
|
||
|
state.names[profile.id] = name
|
||
|
|
||
|
// if following, update id->name map
|
||
|
if (profile.id === sbot.id || profile.assignedBy[sbot.id] && profile.assignedBy[sbot.id].following) {
|
||
|
if (!state.ids[name]) { // no conflict?
|
||
|
// take it
|
||
|
state.ids[name] = profile.id
|
||
|
} else {
|
||
|
// keep track of all assigned ids
|
||
|
if (Array.isArray(state.ids[name]))
|
||
|
state.ids[name].push(profile.id)
|
||
|
else
|
||
|
state.ids[name] = [state.ids[name], profile.id]
|
||
|
// conflict, this needs to be handled by the user
|
||
|
state.actionItems[name] = {
|
||
|
type: 'name-conflict',
|
||
|
name: name,
|
||
|
ids: state.ids[name]
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function updateMyVote (msg, l) {
|
||
|
// myvotes index
|
||
|
var row = state.myvotes.sortedUpsert(msg.value.timestamp, l.link)
|
||
|
row.vote = l.value
|
||
|
}
|
||
|
|
||
|
function updateVoteOnMymsg (msg, l) {
|
||
|
// votes index
|
||
|
// construct a composite key which will be the same for all votes by this user on the given target
|
||
|
var votekey = l.link + '::' + msg.value.author // lonnng fucking key
|
||
|
var row = state.votes.sortedUpsert(msg.value.timestamp, votekey)
|
||
|
row.vote = l.value
|
||
|
row.votemsg = msg.key
|
||
|
if (row.vote > 0) attachIsRead(row, msg.key)
|
||
|
else row.isread = true // we dont care about non-upvotes
|
||
|
}
|
||
|
|
||
|
function attachIsRead (indexRow, key) {
|
||
|
key = key || indexRow.key
|
||
|
state.pinc()
|
||
|
db.isread.get(key, function (err, v) {
|
||
|
state.pdec()
|
||
|
indexRow.isread = !!v
|
||
|
})
|
||
|
}
|
||
|
|
||
|
function follows (a, b) {
|
||
|
var aT = getProfile(a).assignedTo[b]
|
||
|
return (a != b && aT && aT.following)
|
||
|
}
|
||
|
|
||
|
// exported api
|
||
|
|
||
|
function fn (logkey) {
|
||
|
state.pinc()
|
||
|
var key = logkey.value
|
||
|
sbot.get(logkey.value, function (err, value) {
|
||
|
var msg = { key: key, value: value }
|
||
|
try {
|
||
|
// encrypted? try to decrypt
|
||
|
if (typeof value.content == 'string' && value.content.slice(-4) == '.box') {
|
||
|
value.content = sbot.private.unbox(value.content)
|
||
|
if (!value.content)
|
||
|
return state.pdec()
|
||
|
|
||
|
// put all decrypted messages in the inbox index
|
||
|
var row = state.inbox.sortedInsert(msg.value.timestamp, msg.key)
|
||
|
attachIsRead(row)
|
||
|
row.author = msg.value.author // inbox index is filtered on read by the friends graph
|
||
|
if (follows(sbot.id, msg.value.author))
|
||
|
emit('index-change', { index: 'inbox' })
|
||
|
}
|
||
|
|
||
|
// collect keys of user's messages
|
||
|
if (msg.value.author === sbot.id)
|
||
|
state.mymsgs.push(msg.key)
|
||
|
|
||
|
// type processing
|
||
|
var process = processors[msg.value.content.type]
|
||
|
if (process)
|
||
|
process(msg)
|
||
|
}
|
||
|
catch (e) {
|
||
|
// :TODO: use sbot logging plugin
|
||
|
console.error('Failed to process message', e, e.stack, key, value)
|
||
|
}
|
||
|
state.pdec()
|
||
|
})
|
||
|
}
|
||
|
|
||
|
return fn
|
||
|
}
|
||
|
|
||
|
function nonEmptyStr (str) {
|
||
|
return (typeof str === 'string' && !!(''+str).trim())
|
||
|
}
|
||
|
|
||
|
// allow A-z0-9._-, dont allow a trailing .
|
||
|
var badNameCharsRegex = /[^A-z0-9\._-]/g
|
||
|
function makeNameSafe (str) {
|
||
|
str = str.replace(badNameCharsRegex, '_')
|
||
|
if (str.charAt(str.length - 1) == '.')
|
||
|
str = str.slice(0, -1) + '_'
|
||
|
return str
|
||
|
}
|