From 8211a8cb783adb91b90941580b208e5ee3a837c8 Mon Sep 17 00:00:00 2001 From: Rogerio Chaves Date: Sun, 12 Apr 2020 16:40:07 +0200 Subject: [PATCH] Access graph directly to process friendships way faster and easier --- app/lib/monkeypatch/ssb-friends.js | 145 +++++++++++++++++++++++++++++ app/lib/queries.js | 95 +++++-------------- app/lib/ssb.js | 2 +- 3 files changed, 167 insertions(+), 75 deletions(-) create mode 100644 app/lib/monkeypatch/ssb-friends.js diff --git a/app/lib/monkeypatch/ssb-friends.js b/app/lib/monkeypatch/ssb-friends.js new file mode 100644 index 0000000..664ea0c --- /dev/null +++ b/app/lib/monkeypatch/ssb-friends.js @@ -0,0 +1,145 @@ +// Monkeypatched to add getGraph + +"use strict"; +var LayeredGraph = require("layered-graph"); +var pull = require("pull-stream"); +var isFeed = require("ssb-ref").isFeed; +// friends plugin +// methods to analyze the social graph +// maintains a 'follow' and 'flag' graph + +exports.name = "friends"; +exports.version = "1.0.0"; +exports.manifest = { + hopStream: "source", + onEdge: "sync", + isFollowing: "async", + isBlocking: "async", + getGraph: "async", + hops: "async", + help: "sync", + // createLayer: 'sync', // not exposed over RPC as returns a function + get: "async", // legacy + createFriendStream: "source", // legacy + stream: "source", // legacy +}; + +//mdm.manifest(apidoc) + +exports.init = function (sbot, config) { + var max = + (config.friends && config.friends.hops) || + (config.replicate && config.replicate.hops) || + 3; + var layered = LayeredGraph({ max: max, start: sbot.id }); + + function getGraph(cb) { + layered.onReady(function () { + var g = layered.getGraph(); + cb(null, g); + }); + } + + function isFollowing(opts, cb) { + layered.onReady(function () { + var g = layered.getGraph(); + cb(null, g[opts.source] ? g[opts.source][opts.dest] >= 0 : false); + }); + } + + function isBlocking(opts, cb) { + layered.onReady(function () { + var g = layered.getGraph(); + cb(null, Math.round(g[opts.source] && g[opts.source][opts.dest]) == -1); + }); + } + + //opinion: do not authorize peers blocked by this node. + sbot.auth.hook(function (fn, args) { + var self = this; + isBlocking({ source: sbot.id, dest: args[0] }, function (err, blocked) { + if (blocked) args[1](new Error("client is blocked")); + else fn.apply(self, args); + }); + }); + + if (!sbot.replicate) + throw new Error("ssb-friends expects a replicate plugin to be available"); + + // opinion: replicate with everyone within max hops (max passed to layered above ^) + pull( + layered.hopStream({ live: true, old: true }), + pull.drain(function (data) { + if (data.sync) return; + for (var k in data) { + sbot.replicate.request(k, data[k] >= 0); + } + }) + ); + + require("ssb-friends/contacts")(sbot, layered.createLayer, config); + + var legacy = require("ssb-friends/legacy")(layered); + + //opinion: pass the blocks to replicate.block + setImmediate(function () { + var block = + (sbot.replicate && sbot.replicate.block) || (sbot.ebt && sbot.ebt.block); + if (block) { + function handleBlockUnlock(from, to, value) { + if (value === false) block(from, to, true); + else block(from, to, false); + } + pull( + legacy.stream({ live: true }), + pull.drain(function (contacts) { + if (!contacts) return; + + if (isFeed(contacts.from) && isFeed(contacts.to)) { + // live data + handleBlockUnlock(contacts.from, contacts.to, contacts.value); + } else { + // initial data + for (var from in contacts) { + var relations = contacts[from]; + for (var to in relations) + handleBlockUnlock(from, to, relations[to]); + } + } + }) + ); + } + }); + + return { + hopStream: layered.hopStream, + onEdge: layered.onEdge, + isFollowing: isFollowing, + isBlocking: isBlocking, + getGraph: getGraph, + + // expose createLayer, so that other plugins may express relationships + createLayer: layered.createLayer, + + // legacy, debugging + hops: function (opts, cb) { + layered.onReady(function () { + if (isFunction(opts)) (cb = opts), (opts = {}); + cb(null, layered.getHops(opts)); + }); + }, + help: function () { + return require("ssb-friends/help"); + }, + // legacy + get: legacy.get, + createFriendStream: legacy.createFriendStream, + stream: legacy.stream, + }; +}; + +// helpers + +function isFunction(f) { + return "function" === typeof f; +} diff --git a/app/lib/queries.js b/app/lib/queries.js index dbee74a..3e1399c 100644 --- a/app/lib/queries.js +++ b/app/lib/queries.js @@ -207,94 +207,41 @@ const searchPeople = async (ssbServer, search) => { const getFriends = async (ssbServer, profile) => { debugFriends("Fetching"); - let contacts = await promisePull( - // @ts-ignore - cat([ - ssbServer.query.read({ - reverse: true, - query: [ - { - $filter: { - value: { - author: profile.id, - content: { - type: "contact", - }, - }, - }, - }, - ], - limit: 100, - }), - ssbServer.query.read({ - reverse: true, - query: [ - { - $filter: { - value: { - content: { - type: "contact", - contact: profile.id, - }, - }, - }, - }, - ], - limit: 100, - }), - ]) - ).then(mapValues); + let graph = await ssbServer.friends.getGraph(); - let network = {}; - let requestRejections = []; - for (let contact of contacts.reverse()) { - if (contact.content.following) { - network[contact.author] = network[contact.author] || {}; - network[contact.author][contact.content.contact] = true; - } else { - // contact.content.blocking or contact.content.flagged or !contact.content.following - if (contact.author == profile.id && contact.content.following === false) { - requestRejections.push(contact.content.contact); - } - - if (network[contact.author]) - delete network[contact.author][contact.content.contact]; + let connections = {}; + for (let key in graph) { + let isFollowing = graph[profile.id][key] > 0; + let isFollowingBack = graph[key] && graph[key][profile.id] > 0; + if (isFollowing && isFollowingBack) { + connections[key] = "friends"; + } else if (isFollowing && !isFollowingBack) { + connections[key] = "requestsSent"; + } else if (!isFollowing && isFollowingBack) { + if (graph[profile.id][key] === undefined) + connections[key] = "requestsReceived"; } } - let friends = []; - let requestsSent = []; - let requestsReceived = []; - - const unique = (x) => Array.from(new Set(x)); - const allIds = unique( - Object.keys(network).concat(Object.keys(network[profile.id])) - ); const profilesList = await Promise.all( - allIds.map((id) => getProfile(ssbServer, id)) + Object.keys(connections).map((id) => getProfile(ssbServer, id)) ); const profilesHash = profilesList.reduce((hash, profile) => { hash[profile.id] = profile; return hash; }, {}); - for (let key of allIds) { - if (key == profile.id) continue; - - let isFollowing = network[profile.id][key]; - let isFollowingBack = network[key] && network[key][profile.id]; - if (isFollowing && isFollowingBack) { - friends.push(profilesHash[key]); - } else if (isFollowing && !isFollowingBack) { - requestsSent.push(profilesHash[key]); - } else if (!isFollowing && isFollowingBack) { - if (!requestRejections.includes(key)) - requestsReceived.push(profilesHash[key]); - } + let result = { + friends: [], + requestsSent: [], + requestsReceived: [], + }; + for (let key in connections) { + result[connections[key]].push(profilesHash[key]); } debugFriends("Done"); - return { friends, requestsSent, requestsReceived }; + return result; }; const getFriendshipStatus = async (ssbServer, source, dest) => { diff --git a/app/lib/ssb.js b/app/lib/ssb.js index 0c2bac4..2935598 100644 --- a/app/lib/ssb.js +++ b/app/lib/ssb.js @@ -23,7 +23,7 @@ Server.use(require("ssb-master")) .use(require("ssb-about")) .use(require("ssb-contacts")) .use(require("ssb-invite")) - .use(require("ssb-friends")) + .use(require("./monkeypatch/ssb-friends")) .use(require("ssb-query")) .use(require("ssb-device-address")) .use(require("./monkeypatch/ssb-identities"))