|
@@ -365,6 +365,25 @@ const maxSize = 50 * megabyte;
|
|
// koaMiddleware to manage files
|
|
// koaMiddleware to manage files
|
|
const homeDir = os.homedir();
|
|
const homeDir = os.homedir();
|
|
const blobsPath = path.join(homeDir, '.ssb', 'blobs', 'tmp');
|
|
const blobsPath = path.join(homeDir, '.ssb', 'blobs', 'tmp');
|
|
|
|
+const gossipPath = path.join(homeDir, '.ssb', 'gossip.json')
|
|
|
|
+const unfollowedPath = path.join(homeDir, '.ssb', 'gossip_unfollowed.json')
|
|
|
|
+
|
|
|
|
+function readJSON(p) {
|
|
|
|
+ try { return JSON.parse(fs.readFileSync(p, 'utf8') || '[]') } catch { return [] }
|
|
|
|
+}
|
|
|
|
+function writeJSON(p, data) {
|
|
|
|
+ fs.mkdirSync(path.dirname(p), { recursive: true })
|
|
|
|
+ fs.writeFileSync(p, JSON.stringify(data, null, 2), 'utf8')
|
|
|
|
+}
|
|
|
|
+function canonicalKey(key) {
|
|
|
|
+ let core = String(key).replace(/^@/, '').replace(/\.ed25519$/, '').replace(/-/g, '+').replace(/_/g, '/')
|
|
|
|
+ if (!core.endsWith('=')) core += '='
|
|
|
|
+ return `@${core}.ed25519`
|
|
|
|
+}
|
|
|
|
+function msAddrFrom(host, port, key) {
|
|
|
|
+ const core = canonicalKey(key).replace(/^@/, '').replace(/\.ed25519$/, '')
|
|
|
|
+ return `net:${host}:${Number(port) || 8008}~shs:${core}`
|
|
|
|
+}
|
|
|
|
|
|
const koaBodyMiddleware = koaBody({
|
|
const koaBodyMiddleware = koaBody({
|
|
multipart: true,
|
|
multipart: true,
|
|
@@ -1121,35 +1140,13 @@ router
|
|
});
|
|
});
|
|
})
|
|
})
|
|
.get("/peers", async (ctx) => {
|
|
.get("/peers", async (ctx) => {
|
|
- const theme = ctx.cookies.get("theme") || config.theme;
|
|
|
|
- const getMeta = async () => {
|
|
|
|
- const allPeers = await meta.peers();
|
|
|
|
- const connected = allPeers.filter(([, data]) => data.state === "connected");
|
|
|
|
- const offline = allPeers.filter(([, data]) => data.state !== "connected");
|
|
|
|
- const enrich = async (peers) => {
|
|
|
|
- return await Promise.all(
|
|
|
|
- peers.map(async ([address, data]) => {
|
|
|
|
- const feedId = data.key || data.id;
|
|
|
|
- const name = await about.name(feedId);
|
|
|
|
- return [
|
|
|
|
- address,
|
|
|
|
- {
|
|
|
|
- ...data,
|
|
|
|
- key: feedId,
|
|
|
|
- name: name || feedId,
|
|
|
|
- },
|
|
|
|
- ];
|
|
|
|
- })
|
|
|
|
- );
|
|
|
|
- };
|
|
|
|
- const connectedPeers = await enrich(connected);
|
|
|
|
- const offlinePeers = await enrich(offline);
|
|
|
|
- return peersView({
|
|
|
|
- connectedPeers,
|
|
|
|
- peers: offlinePeers,
|
|
|
|
- });
|
|
|
|
- };
|
|
|
|
- ctx.body = await getMeta();
|
|
|
|
|
|
+ const onlinePeers = await meta.onlinePeers();
|
|
|
|
+ const { discoveredPeers, unknownPeers } = await meta.discovered();
|
|
|
|
+ ctx.body = await peersView({
|
|
|
|
+ onlinePeers,
|
|
|
|
+ discoveredPeers,
|
|
|
|
+ unknownPeers
|
|
|
|
+ });
|
|
})
|
|
})
|
|
.get("/invites", async (ctx) => {
|
|
.get("/invites", async (ctx) => {
|
|
const theme = ctx.cookies.get("theme") || config.theme;
|
|
const theme = ctx.cookies.get("theme") || config.theme;
|
|
@@ -2963,12 +2960,78 @@ router
|
|
ctx.redirect("/peers");
|
|
ctx.redirect("/peers");
|
|
})
|
|
})
|
|
.post("/settings/invite/accept", koaBody(), async (ctx) => {
|
|
.post("/settings/invite/accept", koaBody(), async (ctx) => {
|
|
|
|
+ try {
|
|
|
|
+ const invite = String(ctx.request.body.invite);
|
|
|
|
+ await meta.acceptInvite(invite);
|
|
|
|
+ } catch (e) {
|
|
|
|
+ }
|
|
|
|
+ ctx.redirect("/invites");
|
|
|
|
+ })
|
|
|
|
+ .post('/settings/invite/unfollow', async (ctx) => {
|
|
|
|
+ const { key } = ctx.request.body || {}
|
|
|
|
+ if (!key) { ctx.redirect('/invites'); return }
|
|
|
|
+ const pubs = readJSON(gossipPath);
|
|
|
|
+ const idx = pubs.findIndex(x => x && canonicalKey(x.key) === canonicalKey(key));
|
|
|
|
+ let removed = null;
|
|
|
|
+ if (idx >= 0) {
|
|
|
|
+ removed = pubs.splice(idx, 1)[0];
|
|
|
|
+ writeJSON(gossipPath, pubs);
|
|
|
|
+ }
|
|
|
|
+ const ssb = await cooler.open();
|
|
|
|
+ let addr = null;
|
|
|
|
+ if (removed && removed.host) addr = msAddrFrom(removed.host, removed.port, removed.key);
|
|
|
|
+ if (addr) {
|
|
|
|
+ try { await new Promise(res => ssb.conn.disconnect(addr, res)); } catch {}
|
|
|
|
+ try { ssb.conn.forget(addr); } catch {}
|
|
|
|
+ }
|
|
try {
|
|
try {
|
|
- const invite = String(ctx.request.body.invite);
|
|
|
|
- await meta.acceptInvite(invite);
|
|
|
|
- } catch (e) {
|
|
|
|
|
|
+ await new Promise((resolve, reject) => {
|
|
|
|
+ ssb.publish({ type: 'contact', contact: canonicalKey(key), following: false, blocking: true }, (err) => err ? reject(err) : resolve());
|
|
|
|
+ });
|
|
|
|
+ } catch {}
|
|
|
|
+ const unf = readJSON(unfollowedPath);
|
|
|
|
+ if (removed && !unf.find(x => x && canonicalKey(x.key) === canonicalKey(removed.key))) {
|
|
|
|
+ unf.push(removed);
|
|
|
|
+ writeJSON(unfollowedPath, unf);
|
|
|
|
+ } else if (!removed && !unf.find(x => x && canonicalKey(x.key) === canonicalKey(key))) {
|
|
|
|
+ unf.push({ key: canonicalKey(key) });
|
|
|
|
+ writeJSON(unfollowedPath, unf);
|
|
|
|
+ }
|
|
|
|
+ ctx.redirect('/invites');
|
|
|
|
+ })
|
|
|
|
+ .post('/settings/invite/follow', async (ctx) => {
|
|
|
|
+ const { key, host, port } = ctx.request.body || {};
|
|
|
|
+ if (!key || !host) { ctx.redirect('/invites'); return; }
|
|
|
|
+ const isInErrorState = (host) => {
|
|
|
|
+ const pubs = readJSON(gossipPath);
|
|
|
|
+ const pub = pubs.find(p => p.host === host);
|
|
|
|
+ return pub && pub.error;
|
|
|
|
+ };
|
|
|
|
+ if (isInErrorState(host)) {
|
|
|
|
+ ctx.redirect('/invites');
|
|
|
|
+ return;
|
|
}
|
|
}
|
|
- ctx.redirect("/invites");
|
|
|
|
|
|
+ const ssb = await cooler.open();
|
|
|
|
+ const unf = readJSON(unfollowedPath);
|
|
|
|
+ const kcanon = canonicalKey(key);
|
|
|
|
+ const saved = unf.find(x => x && canonicalKey(x.key) === kcanon);
|
|
|
|
+ const rec = saved || { host, port: Number(port) || 8008, key: kcanon };
|
|
|
|
+ const pubs = readJSON(gossipPath);
|
|
|
|
+ if (!pubs.find(x => x && canonicalKey(x.key) === kcanon)) {
|
|
|
|
+ pubs.push({ host: rec.host, port: Number(rec.port) || 8008, key: kcanon });
|
|
|
|
+ writeJSON(gossipPath, pubs);
|
|
|
|
+ }
|
|
|
|
+ const addr = msAddrFrom(rec.host, rec.port, kcanon);
|
|
|
|
+ try { ssb.conn.remember(addr, { type: 'pub', autoconnect: true, key: kcanon }); } catch {}
|
|
|
|
+ try { await new Promise(res => ssb.conn.connect(addr, { type: 'pub' }, res)); } catch {}
|
|
|
|
+ try {
|
|
|
|
+ await new Promise((resolve, reject) => {
|
|
|
|
+ ssb.publish({ type: 'contact', contact: kcanon, blocking: false }, (err) => err ? reject(err) : resolve());
|
|
|
|
+ });
|
|
|
|
+ } catch {}
|
|
|
|
+ const nextUnf = unf.filter(x => !(x && canonicalKey(x.key) === kcanon));
|
|
|
|
+ writeJSON(unfollowedPath, nextUnf);
|
|
|
|
+ ctx.redirect('/invites');
|
|
})
|
|
})
|
|
.post("/settings/ssb-logstream", koaBody(), async (ctx) => {
|
|
.post("/settings/ssb-logstream", koaBody(), async (ctx) => {
|
|
const logLimit = parseInt(ctx.request.body.ssb_log_limit, 10);
|
|
const logLimit = parseInt(ctx.request.body.ssb_log_limit, 10);
|