diff --git a/app/lib/express.js b/app/lib/express.js index 0db54b7..0bf6243 100644 --- a/app/lib/express.js +++ b/app/lib/express.js @@ -23,6 +23,7 @@ Client(ssbSecret, ssbConfig, async (err, server) => { context.profile = await queries.getProfile(server, whoami.id); ssbServer = server; + console.log("SSB Client ready"); }); app.use(bodyParser.json()); @@ -41,6 +42,12 @@ app.use((_req, res, next) => { return imageHash && `/blob/${encodeURIComponent(imageHash)}`; }; + res.locals.profileImageUrl = (profile) => { + if (profile.image) { + return res.locals.imageUrl(profile.image); + } + return "/images/no-avatar.png"; + }; res.locals.context = context; next(); }); @@ -73,11 +80,10 @@ router.get("/profile/:id", async (req, res) => { return res.redirect("/"); } - const profile = await queries.getProfile(ssbServer, id); - - const [posts, friends] = await Promise.all([ - queries.getPosts(ssbServer, profile), - queries.getFriends(ssbServer, profile), + const [profile, posts, friends] = await Promise.all([ + queries.getProfile(ssbServer, id), + queries.getPosts(ssbServer, { id }), + queries.getFriends(ssbServer, { id }), ]); res.render("profile", { profile, posts, friends }); @@ -139,10 +145,12 @@ router.post("/about", async (req, res) => { res.redirect("/"); }); -router.get("/debug", async (_req, res) => { - const entries = await queries.getAllEntries(ssbServer); +router.get("/debug", async (req, res) => { + const query = req.query || {}; - res.render("debug", { entries }); + const entries = await queries.getAllEntries(ssbServer, query); + + res.render("debug", { entries, query }); }); router.get("/search", async (req, res) => { diff --git a/app/lib/queries.js b/app/lib/queries.js index 54cce9f..9a4c721 100644 --- a/app/lib/queries.js +++ b/app/lib/queries.js @@ -1,6 +1,8 @@ const { promisify } = require("./utils"); const pull = require("pull-stream"); const cat = require("pull-cat"); +const debug = require("debug")("queries"); +const paramap = require("pull-paramap"); const latestOwnerValue = (ssbServer) => ({ key, dest }, cb) => { let value = null; @@ -43,57 +45,24 @@ const latestOwnerValue = (ssbServer) => ({ key, dest }, cb) => { }; const mapProfiles = (ssbServer) => (data, callback) => { - let promises = []; + const authorPromise = getProfile(ssbServer, data.value.author); + const contactPromise = + data.value.content.type == "contact" && + getProfile(ssbServer, data.value.content.contact); - promises.push( - promisify(latestOwnerValue(ssbServer), { - key: "name", - dest: data.value.author, - }) - ); - promises.push( - promisify(latestOwnerValue(ssbServer), { - key: "image", - dest: data.value.author, - }) - ); - - if (data.value.content.type == "contact") { - promises.push( - promisify(latestOwnerValue(ssbServer), { - key: "name", - dest: data.value.content.contact, - }) - ); - promises.push( - promisify(latestOwnerValue(ssbServer), { - key: "image", - dest: data.value.content.contact, - }) - ); - } - - return Promise.all(promises) - .then(([authorName, authorImage, contactName, contactImage]) => { - data.value.authorProfile = { - id: data.value.author, - name: authorName, - image: authorImage, - }; - if (contactName) { - data.value.content.contactProfile = { - id: data.value.content.contact, - name: contactName, - image: contactImage, - }; + return Promise.all([authorPromise, contactPromise]) + .then(([author, contact]) => { + data.value.authorProfile = author; + if (contact) { + data.value.content.contactProfile = contact; } - callback(null, data); }) .catch((err) => callback(err, null)); }; const getPosts = (ssbServer, profile) => + debug("Fetching posts") || new Promise((resolve, reject) => { pull( // @ts-ignore @@ -130,8 +99,9 @@ const getPosts = (ssbServer, profile) => limit: 100, }), ]), - pull.asyncMap(mapProfiles(ssbServer)), + paramap(mapProfiles(ssbServer)), pull.collect((err, msgs) => { + debug("Done fetching posts"); const entries = msgs.map((x) => x.value); if (err) return reject(err); @@ -141,6 +111,7 @@ const getPosts = (ssbServer, profile) => }); const searchPeople = (ssbServer, search) => + debug("Searching people") || new Promise((resolve, reject) => { pull( ssbServer.query.read({ @@ -166,6 +137,7 @@ const searchPeople = (ssbServer, search) => ); }), pull.collect((err, msgs) => { + debug("Done searching people"); const entries = msgs.map((x) => x.value); if (err) return reject(err); @@ -175,6 +147,7 @@ const searchPeople = (ssbServer, search) => }); const getFriends = (ssbServer, profile) => + debug("Fetching friends") || new Promise((resolve, reject) => { pull( ssbServer.query.read({ @@ -191,10 +164,11 @@ const getFriends = (ssbServer, profile) => }, }, ], - limit: 500, + limit: 20, }), - pull.asyncMap(mapProfiles(ssbServer)), + paramap(mapProfiles(ssbServer)), pull.collect((err, msgs) => { + debug("Done fetching friends"); const entries = msgs.map((x) => x.value); if (err) return reject(err); @@ -203,14 +177,25 @@ const getFriends = (ssbServer, profile) => ); }); -const getAllEntries = (ssbServer) => +const getAllEntries = (ssbServer, query) => + debug("Fetching All Entries") || new Promise((resolve, reject) => { + let queries = []; + if (query.author) { + queries.push({ $filter: { value: { author: query.author } } }); + } + if (query.type) { + queries.push({ $filter: { value: { content: { type: query.type } } } }); + } + pull( ssbServer.query.read({ reverse: true, limit: 500, + query: queries, }), pull.collect((err, msgs) => { + debug("Done fetching all entries"); const entries = msgs.map((x) => x.value); if (err) return reject(err); @@ -219,15 +204,32 @@ const getAllEntries = (ssbServer) => ); }); +let profileCache = {}; const getProfile = async (ssbServer, id) => { - let [name, image] = await Promise.all([ - promisify(latestOwnerValue(ssbServer), { key: "name", dest: id }), - promisify(latestOwnerValue(ssbServer), { key: "image", dest: id }), - ]); + if (profileCache[id]) return profileCache[id]; - return { id, name, image }; + let getKey = (key) => + promisify(latestOwnerValue(ssbServer), { key, dest: id }); + + let [name, image, description] = await Promise.all([ + getKey("name"), + getKey("image"), + getKey("description"), + ]).catch((err) => { + console.error("Could not retrieve profile for", id, err); + }); + + let profile = { id, name, image, description }; + profileCache[id] = profile; + + return profile; }; +setInterval(() => { + debug("Clearing profile cache"); + profileCache = {}; +}, 5 * 60 * 1000); + module.exports = { mapProfiles, getPosts, diff --git a/app/lib/serve-blobs.js b/app/lib/serve-blobs.js index 16c97cb..9f63e7d 100644 --- a/app/lib/serve-blobs.js +++ b/app/lib/serve-blobs.js @@ -4,11 +4,13 @@ const toPull = require("stream-to-pull-stream"); const ident = require("pull-identify-filetype"); const mime = require("mime-types"); const URL = require("url"); +const debug = require("debug")("server-blobs"); const serveBlobs = (sbot) => { return (req, res) => { const parsed = URL.parse(req.url, true); const hash = decodeURIComponent(parsed.pathname.replace("/blob/", "")); + debug("fetching", hash); waitFor(hash, function (_, has) { if (!has) return respond(res, 404, "File not found"); @@ -22,17 +24,32 @@ const serveBlobs = (sbot) => { // serve res.setHeader("Content-Security-Policy", BlobCSP()); + respondSource(res, sbot.blobs.get(hash), false); }); }; function waitFor(hash, cb) { + let finished = false; + let wrappedCb = (err, result) => { + if (finished) return; + finished = true; + cb(err, result); + }; + + setTimeout(() => { + debug("timeout for", hash); + wrappedCb(null, false); + }, 1000); + sbot.blobs.has(hash, function (err, has) { - if (err) return cb(err); + if (err) return wrappedCb(err); + debug("has is ", has, "for", hash); if (has) { - cb(null, has); + wrappedCb(null, has); } else { - sbot.blobs.want(hash, cb); + debug("calling want for", hash); + sbot.blobs.want(hash, wrappedCb); } }); } diff --git a/app/lib/utils.js b/app/lib/utils.js index 1a4e644..8914cc4 100644 --- a/app/lib/utils.js +++ b/app/lib/utils.js @@ -13,8 +13,11 @@ module.exports.promisify = (method, options = null) => { }; module.exports.asyncRouter = (app) => { - let wrapper = (fn) => async (req, res, next) => { + const debug = require("debug")("router"); + + let wrapper = (debugMsg, fn) => async (req, res, next) => { try { + debug(debugMsg); await fn(req, res); } catch (e) { next(e); @@ -22,10 +25,11 @@ module.exports.asyncRouter = (app) => { }; return { get: (path, fn) => { - app.get(path, wrapper(fn)); + app.get(path, wrapper(`GET ${path}`, fn)); }, post: (path, fn) => { - app.post(path, wrapper(fn)); + debug(`POST ${path}`); + app.post(path, wrapper(`POST ${path}`, fn)); }, }; }; diff --git a/app/package.json b/app/package.json index 726bd95..9706b3f 100644 --- a/app/package.json +++ b/app/package.json @@ -4,15 +4,16 @@ "description": "", "main": "index.js", "scripts": { - "start": "electron .", - "start:user-2": "SSB_PORT=8009 EXPRESS_PORT=3001 CONFIG_FOLDER=social-user2 electron .", - "start:user-3": "SSB_PORT=8010 EXPRESS_PORT=3002 CONFIG_FOLDER=social-user3 electron .", + "start": "SSB_PORT=8009 EXPRESS_PORT=3000 electron .", + "start:user-2": "SSB_PORT=8010 EXPRESS_PORT=3001 CONFIG_FOLDER=social-user2 electron .", + "start:user-3": "SSB_PORT=8011 EXPRESS_PORT=3002 CONFIG_FOLDER=social-user3 electron .", "clear": "rm -rf ~/.social; rm -rf ~/.social-user2; rm -rf ~/.social-user3" }, "author": "", "license": "ISC", "dependencies": { "chokidar": "^3.3.1", + "debug": "^4.1.1", "ejs": "^3.0.2", "express": "^4.17.1", "pull-stream": "^3.6.14", diff --git a/app/public/images/no-avatar.png b/app/public/images/no-avatar.png new file mode 100644 index 0000000..2abcc8e Binary files /dev/null and b/app/public/images/no-avatar.png differ diff --git a/app/public/style.css b/app/public/style.css index 5079591..371f3fa 100644 --- a/app/public/style.css +++ b/app/public/style.css @@ -2,6 +2,7 @@ body { font-family: sans-serif; margin: 0; line-height: 1.3em; + word-wrap: break-word; } header { padding: 0 30px; @@ -55,15 +56,18 @@ main { .columns { display: flex; + align-content: stretch; } h1 { text-transform: uppercase; font-weight: bold; + line-height: 1.3em; } .wall { padding: 20px; + flex-grow: 1; } .post { @@ -90,3 +94,11 @@ h1 { font-family: Georgia, Times, "Times New Roman", serif; font-size: 18px; } + +.about { + max-width: 300px; +} + +.profile-pic { + width: 300px; +} diff --git a/app/views/debug.ejs b/app/views/debug.ejs index 051c38e..86ca861 100644 --- a/app/views/debug.ejs +++ b/app/views/debug.ejs @@ -1,5 +1,13 @@ <%- include('_header') %> +
+
+ + <% entries.map(entry => { %><%= JSON.stringify(entry) %>