From 42062a6fc474fb9aa3fc72b087f4c4ebc05eba8a Mon Sep 17 00:00:00 2001
From: Rogerio Chaves
Date: Mon, 6 Apr 2020 20:41:13 +0200
Subject: [PATCH] Default profile pic, more logging, a bit of speedup with
memory cache of profiles
---
app/lib/express.js | 24 +++++---
app/lib/queries.js | 106 ++++++++++++++++----------------
app/lib/serve-blobs.js | 23 ++++++-
app/lib/utils.js | 10 ++-
app/package.json | 7 ++-
app/public/images/no-avatar.png | Bin 0 -> 9343 bytes
app/public/style.css | 12 ++++
app/views/debug.ejs | 8 +++
app/views/profile.ejs | 12 ++--
9 files changed, 128 insertions(+), 74 deletions(-)
create mode 100644 app/public/images/no-avatar.png
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 0000000000000000000000000000000000000000..2abcc8e4de2d4f7eeb9c7ae4ede3e76b9324e631
GIT binary patch
literal 9343
zcmaKSby!>9(r%FAUJ6AD0SXj%C%Bg4?i!>J5*&g{ahIZ{SaG+a#oY_V-Cavd(VL#{
z{LXj(xaaQYNwz#Qvu3X~nf1;)iBMCK$HgMY0ssKGiV89sh_mhA4+b*gJJFB!7veiF}5barO2QARf3Ph{Rr_8AglLp(`DZrhW?gBJKN5m$Qq8)hB||R
zeC(WTmYn>YKz=TEE*@S10RdJZHzyY-2PZcN7dIQ1fG`iYFefMQ-v=#XG-oSoVGS9%
ze~*Q@1JT;Ly26AxI6OQ&*gbgIq0TlOTtY%Ze{FDcvmqqdT)Z4z%{|#1UFiO?AOm)>
zbhd-J+Cd$Ge=V9@K;2wHv8MnYr4Nf!Gxuq!RD?|XDuky;h$MlvxT}s
zU2LH+ptL3*P}$tl&hhX4zvPvbg%urLT+JOV!HO~Vafd6`BVY~nC3)g?O_itUR|LzO-f7RtcIK%OGVgIj%{^t=Qdj4MiCu|Wn{|S7s
zBO=zzrQ7>hQvz=V=BJEL`5ZvCXb{^VDBEp
z!1$g}v%ZdTI2gBiIhWoL>JL*h3J4ztXnaw6o=hTz-5xSY^qq$0nTBcWlgEVL8Ej7y
z*~<&5f5*6dU~GhK&&MVedR8|P0lT`ztux@HP%d4ITf}cLU{fSgL6yvb@-ie1*?z2l
zKX$f1xR3tg@a6J?R+UC7+CIClRzI@w#Hb3L_GDZ%7E}2gqn6B{j
zfH&p~p;V+?5cGL(MMEs}Wktb%sp)s6U{kpYw)(!TBc%DYOBxG724+B5h
z7f%EhE1<}nZ@{L<%8<2*TA9Tj{OJYiOFZ>NSPHrLra;q@6?cw$6V(I{xjiiPb^4Aq
zmRtkc@4>a{6KfuvdXkPpbb~(0d?$E^V9>0t;O}PJno(%9Tp7)z?Iawd!C=N%_AJ
zw(@&v&3Hx`ubtx-8LNnA;TXBO@i|ehq6ZA#I?N1(QGM#->%nebv+V`1pwQnCpobpOW^b1#vVtdwF`w$jH>&PSJ95#w+Hjm!(wjBW8TWiv(BI
zlBMd3Ud0S9n<3>p`Mx1uh#eW;P9alLUtgcsR=N&1(br%2QKpMeDHxxX70uyF#cEkBRdxRkF2p`J~b~m3NDmYjegej$`dX$FY{@l%e~9
zk_-n;Um*Qyw-HC}eFOVGRl%0E`p{4kV;rTBhMJn1re$eqDKiI0Re5cbn>(|05n_=h{yDgVVmj`E=FW}4T8!%8myI#JkpXlpEJw==^*lD?1w}==Iy!%DZ*T9O-TnC^8IDK{
z=X5^_2ayNb?(IoAOhRy^6pP-af3Gryik24?{2WdxOJz3}_B>yIe7HOBY&@Y&N=zgo
zBBG`3HZn4Tad2uARX#DS2atAlGZKVsuA_oRM+XBRf^v4Yw*vy@<>dMY1_t{3(NJ%C
zUH~|DWwmhLrwnGG+_;}EW3rvDb@+$jO~q4@VPW1@GPAU%WMs71P4BL>di3B)F^A#Z
zNf)Vn0)U2I3An_dn&tz-xX@uCz!{S6XGlUm`+9zUV$pCPAKhZAmcyeXCPPq(92@tQ
z4cBzU@#_o9bn`(4vKZ?av>qTAe?Bd2N42grFAx6oh+@idvwObLV@~+E)gPnu?p?^D
z215^f`b869U`1Rd+^3UjXib4UX#r4#H3ax7nC9$o^zqXHX{Ag9_Q7O?lk@T;S;
z#bCOXuU+zr@#OLsrBrr%uWb*&;7<1@P~S(urd
zI8b6IwJLN{GvN12vTM?c-oFn;9PiVbngnTYDY*yKwo2u{ZY8as|N2EG;`sVoCg0H$
ziSJc=*|(t)x=^^ccehq`bSR$~V_OrZqwW0DYP|=&y4sKF-dv{FzivXd^3MUE(JX(o
zG#rtPnz}x~r&y|@{q9bfAWIE33VcCtmG5JZFw%2xV@2Jl3X`h0-ckJI;^*&QDqF}6
zE@5I~$^**P<6Ql+W7bENi+lEE^wQN)I;7ue{amp$(xsSrhTuWJW;5*16-m&h%4adl
zawnI__hThp57IF<#krCDJRAu<8-N_~Cww{=@?_Gku$oXe=F4dd!&m3jKBL
zcCxfY&}i>uv4>lP!}Fc#QS6z$$u4Q(PfD%s5W-@n1X>8BDaM6Bu$GWF(W*$maaj<$
z+NQ&{r2J$Cx6lacL$+j$$I^@p$$S}`gHpkmCg`43-u2nOfgC1p%h~Dd;vx>Yx`ha;Bkt2#F;C`w6*%9jZg++O8wbBP8#XNYFu5_yOdOQjwGEBAfFP^<5
zQ1}e*?$w=4bFtyzcrwS8GB;A0kzshpu#tciM$%c_4vPb
zL_8c%78@>%KT`?+j8g_R;9JZgrT6HB<`H8Z2+Fe94X7P5B03BxkCnuP-D3
z{m6MdgLjAi_ILzWg{X1`jWyS
zTncTiKKFy=63rrPQuYEWkcv9G-_@YTUMGgKqLLB?2>wn%p}5QJ`+?P4-2t|~KKeD^
z-z$xfB~cIv5qLu2;p6~-IQqL@L{_qy$Zq#IGpRmR&zEa|dc3o`JjGMk@12<7;NrTN
zDVZJ`LPrx_!!Lc$O|kAn9n;GNT0to`3-I+3)jwU+8&DXo(@fOT(n6(6`TQAQKvz$1
zKTY?URhe!T+Rtatp56UE8^GLAQlL6m*C(KSeEg=Us2ENy!Oz3Z?Wyu*DlormJE`EO
zzK;9dWUjP2WXUzwOG-;~TpC;6w9KqbmKyqbD!>2Pv(TnyczxXI{>e_6^BXJFsftSe
zl+?Mj=%CijqSjV)b==EFR~RYH#D1yP1|ge;rDbjHwriWHoxRVS(qTpM&;G%_AF`#a
zZhxP}*#@%6u2^HI;!xiBC@5Uj*47pm7v})|?fKiX2RZ{$I0{W4|LlF5nwkQ}QBj=|
ztIQ+}|GvIHLHOtFe3`JB|gjM_zt|??4c)Z6#Lu=FQwEUO)ong@yNuIQC8DqA
zSjb|9jI#+NZSMHk0l=tepdD-vK4h#Xc3y39!{%Pglg0M;_t$$se@!BKpg3MOUO`PV(QTq-xJzz
z#f^YW;9T=BjQzqlbkLVSQ91*8Qd3h!zz81RTwl7aQzmdw=SmwLBMtEyHUK$;cY|3=l?+1_w%0U3;b#^GdC
zEfmq}d&f`fj`2z{t)kE3gRyYawxna;H7
zrFQc^;H}w4-ur6$=^a}G%s!;{hyDrxN<%{kuh)ttQ`
zYNEKU63MqFmXl^o;Xy3)7f#nWX2^2!?)7B5Uubx!u3hl9Nfg?Uo@X(knt<8#h?=#;
zviW284sxhD#{jV}AmYh0LNBl`{|K}R0)d^j<>Ny`je53`GOhsng+GsK&o_0q%3FX?GJxPMHnlr1bt2bOA(8|Zf)Cvy$=U@o>2NA8S
zymv$2-*Z2HK_M#GPHB9WKzxoJ!WuZjhGTVddwm2edFc#(A+g24eV{D*5H*MX?Af0`
ze_jbW7nGNe&dtf&;d&jAnQG?aR3wiGQu6SME{wF3;D5w_C5((e`OWvvdaUiFiCE!m
zXCyV3g@%R(PnTe3Noc-?s{X}RD8^Iw2keD~h2{DSv?&X;R$;-)>>1SzOE-WBoRwPP
z(N{-jJWI$$>~^1yxk$&r#7N)BC@U#x>*y%z=9Xqm=uJxU6Gqiyv&&v$Vj=?2R)?<~
zLNH(9p^s%w;s8n*b!m&vaiqesBWRZEY$g!iTwYy8c)_rP?Fo$X8|q)9pZF-CrfC|f
zD6i{TT3ASg`#oIida{hKC-*75Z4S=3zZ~KC@Zm$7$zToV61&piUbfTv1_7_cbYq1P
z!2pkfATtXKSEnswFD_fra9!92WpWMDLwKm1oSgf?ckt8W10t2|Xz8c>Zej#6Z`G3V
zShE^65mf9YeurU?)#dK&?6kU{NWHl|uA7jPmrs&&aRml*4sgO$MK3~LF+!I0QtVm2
z{;nS`MIcPI_?elR+S(xk8T66sizYfswMg#gx{bC+KP)8ujkM>E#;jTAlkcSW&p+VYyTv$lXbf@o1Vx7X$NUbZ)0qxEdLJ{cKVaGNO7;7F)w>196gJCk;`f@K0N&V1`
z60`>ZS!r?mb-Yk55)uJfN$Gtbv7;~ZOI0nh?YoE*J)QpeU{j;>PF#ArkSgfGYR#|R
zoMb-HKCs!-D{|}m7o|xHK~3A5VV%@{O6`>AK61g*2`<`E)$ui3a&&%=cWY0vV)GRu
znzNM|qaS+I_@`^y`}mhr!`N4g^*C1u?N6
ztley>5yJPQpS?!4x%tRz>(RwHuy%&fY@pNAHX7Chxp*ebJ8
zc_>6Rl>SbxFjvaJ;5O(F_TE?~`rdgz1vHw9hv+8;)O>E;Vwo1QQ0q3H#bKsl@`*R0
z@Z(EqXPxFpZ|XEcmH{SfsmyqR$`KDm|~p1kiEeNWTxS;V{elSs`qhLghONmNeGu@+AaH
zd?)b+8T;p@gZpN0$@T*iQlQvNLlwyv^>W!XR!bDcmckwv7k2JfyJHVS=y=q^$O}wu
z>KGjk{E9e^e1Zdd-vP+K!3&1t^cTy5wHM|Z4oj9X?`R2vWc
zqun$bjqz1HKorUNLv+9`opq3lDB?%+B})H>ffWbl`ijf>CP`IHapaO_x;)arq&hL%
z?*Q>Yzrh(dnRIil&Fku>Qk9W`K~7e7iXB5GH@^EiC^J#*OX4AR)`#KtDUd4;b8weY
zjYXf$=SP&yUtc?XLrsQ8_+(l%WRHXe!;s@NqokZJDm05!s72h5_owph?Ccg6nwfPj
zSf4xKb7ZZP5LmZ8`y4KOu56_x^ZRrqDfFVJ4)fJD
zE_?F@1+RPAFwE-es>~GtYXL|oo0~1whq?=Xob9u<-LH5~7$p9*%Aw*JceB``BO4qk
zMFBhXZ$LQi&+)(Fi{Ha~!wHo$nx>?Lz*vvsyXW*NU1ou%V*Zb!_4r(XJe7Mr4UN$8
z9LAUUGE9gP1Vf63J3?_qT1u*TTkjxNnZZj3>7}GJA*sj7;&qk(13)8`!CVW0?&p4;
zEL9mbBjbs>?oawX+&)7`-x{i{WKT|;JaS7G@ad#1+UlIjP%4$nvEpTW)rYC&_IUrt
z&kw13xF;+I1M4y=U#?3*vQpV=hXPYAdrxvi68JW9#IX(~Un(6LgHH>`81h|)qe`M9
zDXwnQ)Bw&R!s5{rimUXIm5H7GR6K{{#@*+B`sX{(f(-JWi06#66)S*G0q1z+yteb_
zsRZ^Yf*ID}45iA(gh0&&5&~pN4T;4TfDV4g?P+V@)BWDwsUuZY`!C^RV?-8^X=q!U
zFdm(jGMtas>ejsYh%a6{qOkKNGO`p=c9xrV_-%AU1IVQa_39C&O>q3(?QL~mJM*_2Q)paZV&Cxa*cFJpuKKHs696Y|37yudX
z`QEG6b(@RM`T2SEeEC{m
zwA89GEp(Pr$Qd;rvReNN6cH6g!M!cZi?MA#^t~@!^~xEW&>H{cB!}2zI)Y4VZwECS
z?=h)z`?OhSu@VarDXxYVAQx=+UQCDKQ?IsqOf=ZfwY9Yg54av9n5VUGz9=ZZT~Nh~
z=8P?_`ZF}8e!V%#4OCTDrpg-xr1*w_FQ=fE@ieY{=`cJ84~!lZIbvMoSWy(U;1P5>
zMEy%fdswsu?>KK-mKCIoV&gyWcWW-O8-=g>2Zn_Wwwz?$5&E97a
zuHVz9=owSM3i#^P(RbKvS+LUkGqizedlRO5EmnV8+$eG~>EvU{h2C=xwtW5i^=-{5
z1v6T<)6KcMna9oKR8Y_nygd`j;~yU#&}r?-Z8b14fln*FvCDE?Gv2E)X}#8`ZIco>Qksb)ehAQLcd^K={CzW^p`7Ja+gV@fJnj^obx8Syb+
zTWh1rXXGJpE7NCQLxLfQXySuWz;6zNpDXFpdo3PkTz22>RYBLp!g9fdqXFO5da_WW
zri&)1K+ddeCsHaZR3n?(lC=TY&7`jk=R-n+WVcY;r~|rXf*!V>=gOL?@lf~Lk7Zn7
zdJUBNu_j6q1Ca_>I!-gW!EY56FT$uFCibVQ&NcYq+;U19l-Gb*C{_;*<1UhEGi0d|
z!98|s;gfM&%@nH}TWq-sA0bO+3~!=k|B*a$!9kloS$ctia_NkHj`@a0Q?R7C*erVH
zMSO1am`{6`cVI?U6$dRRNF<H7)@=t37pM;<<{6=&(>a52iBMxNn
zi*TLJ6|+LKyg>n5Du5GfzWPf+=|)n&2guvESeq};3rW#H)RdHiuSaC`Nm}0(+~)(M
zn9W%y89_N#MG;j4eL8)r-BuHRSGw*~1#z`aCYXqa5FEEY`76
ztjF&O3PS}^+6{KO9HJ;i)=v#lq61T)W;ll5JSR?lUG~O7UY9dh6XN1tmzhqgZstK|
zY|^c*-{xlN13AG{L2joh6?wXLZtU&>oS$v;Mg}pNLylE$br)i8Q#BNNL(JxuGn-WW
zj$*b3;-2cg>=Atf6W?p?US?tpu7NMDHjk&@#=Azhb>x3y#btx;Twb2lSPry3-X0|+
zu(9blLu*pT?NS;zhwTWLaunYx_L{x9BAyY|iMA*KTM}KIVww(cQ#yE-mtf)S_2U{c
zyi429BPC_k3kXLJlG>s4fpUsOJU>hIn4Txv;R9w#;AT56*8jHl5d?9Uez8j7KGG>A
z##G0tGD7a+ouWNMqiMC~dqC;M;S&-XePZfNDMGjH(OIlZ8Dmi#-V8zmJCSMT#;^ii
zUzYav79czg?x`|q7iM9J%8E0FNP%{>nTms!X}@KHyxrB+r=rQI1FzN0u`Sa`GO<$y
z-x=+Dwuj5HVe!A(nC^1NG!gkx2MU$x$#`tH(GHb%bMc6#%f~G(R5lL7dxhnW*^-@+
z@s>h)a(?aVg@U=@|e*0XOSUYAGgx2?+apVlJ`oxn^9
z4!5XB;NkSrRu;IR&oUY3R8#)oaCvgi(rmSmq6Ubn3?
zTpEzWsHDmix$NRF$+4pJMcodU5jck3sOfxM1ge^TX`?GpQHNzibz&YS7_+(F6-*dI
z34*&Kmeb+6o-%~pQ0vzETP8IJ7c)zR-u**ja&j;t#5RwrI?!gK`^m?k7f|tdARQ2i
z?HS_}f}5+Wt6QLbSyH)3xCrkr&5mwOQq`}sUYV^Bf&V(jY~4NDlU@3xSh_YId+AQ$
zyRko4Wol$Z*>o6)g0ANqsFfPCB}v~UzW`en@?V`pWE=lKBc{TxwyNI8LmYMYh3?6^
zo5=V}h~AN8Fj{V1CO>3&bhI2&lU-c?HgD#|2|uFm6H9>FHMu>M^qeUBhpjeo6HPg>
z>=+clO3$Hbv3GhZY}Oet{z*jI5rI;QvVfI8`WJpR6HC~9H8uqbCy?v@+3L|AC%v|jSLSDn4Ee<{|!*V`=UkcU=SX)v-5)W
z*4pV0Wb38|+V2rV7blrB@=ZTx{)dKXgy-_>DAI;`2<1>WM{_JKckT85l%-@hqDg0J
z49TgECVi^w(5FjR4kfoJHG@sgBKSB4Mn*)NZGDHZ=TM2`)3e@u?q`?%wP+}uK`HXR
z1xK7YD^@|Z=QGwnzWg@6vL=`(3T^X{_qB0jz!iNm&G
z2|IrB5Zv0CE7+{d;hj$gQ{fxHycZl!8xKh4bm{Bs8(xm?ci=VPbv|U#7sGQ!Y|*+^
zCh+!Ehmu5ko$sqI6CW3|LhC~EYF^nxPKtQRJ!-2x!cB53%v{zR{rvn2DxA%jH3`(U
z4K!L@_Edn^VO`P)lEA40INTTdyZm!iru$4;Zp=d#yeMgov4j2Efs>KMznt8Wiki|J5RrV|1JUFfZO#4+f$
zmGY(zUG%7HFSD&}8y9~9K}#X}v5o4@@4R=g$ju@?2hv;8$Z0tL3S%pkRkzg&pi1%Z
zi(mhI2Bd-ZQoh1PLRI6m*UB#pJOUxgl6OEoZkx1Zd#Ah@7ZM5lur;PT6(^SP@Uk*{
zIXSfTT4Y@l6BVT+$u;8VjwJjkY`l%7V$8@sYiiL?xYENEfery(DapxNIywd{%=WAr
zIu{j_Ie^cFRKn`T{Db^>vt!zfPYt9Y7Ao)6*-(I@+2iu@(F{k)oP%L+U#xFM`J254
k%k~8+X2T!-k
+
+
+
+
<% entries.map(entry => { %>
<%= JSON.stringify(entry) %>
diff --git a/app/views/profile.ejs b/app/views/profile.ejs
index 9b8e05a..c8f694a 100644
--- a/app/views/profile.ejs
+++ b/app/views/profile.ejs
@@ -2,10 +2,10 @@
- <% if (profile.image) { %>
-
- <% } %>
-
<%= profile.name %>'s Profile
+
+
<%= profile.name %>
+
+ <%= profile.description %>
Friends
@@ -31,7 +31,9 @@
<% posts.map(entry => { %>
-
+
+
+