Pārlūkot izejas kodu

Oasis release 0.4.8

psy 3 dienas atpakaļ
vecāks
revīzija
4d3fe5da35

+ 15 - 0
docs/CHANGELOG.md

@@ -13,6 +13,21 @@ All notable changes to this project will be documented in this file.
 ### Security
 -->
 
+## v0.4.8 - 2025-08-27
+ 
+### Fixed
+
+ + Fixed legacy codes (invites plugin).
+ + Fixed SHS generator (script).
+ 
+### Changed
+
+- SHS CAPS (for private gardering).
+- Deploy PUB documentation.
+- Invites.
+- Banking.
+- Inhabitants.
+
 ## v0.4.7 - 2025-08-27
 
 ### Added

+ 21 - 13
docs/PUB/deploy.md

@@ -27,10 +27,10 @@ Paste this:
 
 {
   "logging": {
-    "level": "notice"
+    "level": "info"
   },
   "caps": {
-    "shs": "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s="
+    "shs": "iKOzhqNVTcKEZvUhW3A7TuKZ1d6qIbtsGIJ6+SBOaEQ="
   },
   "pub": true,
   "local": false,
@@ -40,22 +40,19 @@ Paste this:
   },
   "gossip": {
     "connections": 50,
-    "seed": true,
-    "seeds": [
-      "solarnethub.com:8008~shs:HzmUrdZb1vRWCwn3giLx3p/EWKuDiO44gXAaeulz3d4="
-    ],
-    "global": true
+    "seed": false,
+    "global": false
   },
   "connections": {
     "incoming": {
       "net": [
-        { 
+        {
           "port": 8008,
           "scope": "public",
           "transform": "shs",
           "external": "{your-hostname}"
         },
-        { 
+        {
           "port": 8008,
           "host": "localhost",
           "scope": "device",
@@ -63,15 +60,19 @@ Paste this:
         }
       ],
       "unix": [
-        { 
-          "scope": ["device", "local", "private"],
+        {
+          "scope": [
+            "device",
+            "local",
+            "private"
+          ],
           "transform": "noauth"
         }
       ]
     },
     "outgoing": {
       "net": [
-        { 
+        {
           "transform": "shs"
         }
       ]
@@ -93,7 +94,14 @@ Be sure to replace {your-hostname} with your server’s domain or IP.
 
 ## 3) Install ssb-server and plugins locally
 
-   npm -g install ssb-server ssb-master ssb-gossip ssb-ebt ssb-friends ssb-blobs ssb-conn ssb-logging ssb-replication-scheduler
+   npm -g install ssb-server
+
+   mkdir -p ~/.ssb
+   cd ~/.ssb
+   npm init -y
+
+   npm install ssb-ebt ssb-conn ssb-replication-scheduler ssb-blobs ssb-friends ssb-logging
+   
    npm audit fix
    
 ## 4) Create the launch script and some patches

+ 0 - 29
docs/PUB/invite-codes.md

@@ -1,29 +0,0 @@
-# Oasis PUBs Invite Codes
-
-Below is a list of **community-generated PUB invitations** you can use to join and find more people. 
-
----
-
-Invitations are limited, and PUBs are self-hosted servers, so they may no longer be available:
-
-- PUB:"La Plaza" (solarnethub.com):
-
-"solarnethub.com:8008:@HzmUrdZb1vRWCwn3giLx3p/EWKuDiO44gXAaeulz3d4=.ed25519~+WFcDtA3E8pKgRd/FzIstZkwo4K1GEVkeJWKCPhmK5I="
-
-- PUB:"Oasis-Project" (oasis-project.pub):
-
-"oasis-project.pub:8008:@JE5RwEExtXpmKAeJuPl6/OQjj53O+hCFU0KdyzeNJIE=.ed25519~xRWxsSftlXN3gktk5oGS/3IbhICkh0IXSJ0B+7jAGlA="
-
-- PUB:"The Pirate Oasis" (thepirateoasis.com):
-
-"pub.thepirateoasis.com:8008:@70JEzyx6wJGaihhiGzQCANBboV1h0OQHtaQ1ST7FDac=.ed25519~nHSrNVc+ACyNBIkaXTerW/SwE+6h2ug6H7h3+ETTpkI="
-
-- PUB:"Artivismo" (artivismo.net):
-
-"pub.artivismo.net:7723:@XPcWSwd8555YSXQiIR04RY8rOVGTROhy8fpqLmn8rjo=.ed25519~pMO6O+AGSF/u0U8HWOxRV25haDV9psMoqw52+LOK1GA="
-
----
-
-Contribute by **deploying your own PUB** and adding it to this list. 
-
-The more nodes we have, the more resilient and uncensorable our content will be.

+ 4 - 2
scripts/generate_shs.js

@@ -1,2 +1,4 @@
-const caps = require('ssb-caps');
-console.log('SHS (caps) Key:', caps.shs);
+const crypto = require('crypto');
+
+const cap = crypto.randomBytes(32).toString('base64');
+console.log('New SHS cap:', cap);

+ 46 - 38
src/backend/backend.js

@@ -239,7 +239,7 @@ const forumModel = require('../models/forum_model')({ cooler, isPublic: config.p
 const blockchainModel = require('../models/blockchain_model')({ cooler, isPublic: config.public });
 const jobsModel = require('../models/jobs_model')({ cooler, isPublic: config.public });
 const projectsModel = require("../models/projects_model")({ cooler, isPublic: config.public });
-const bankingModel = require("../models/banking_model")({ cooler, isPublic: config.public });
+const bankingModel = require("../models/banking_model")({ services: { cooler }, isPublic: config.public })
 
 // starting warmup
 about._startNameWarmup();
@@ -365,26 +365,38 @@ const maxSize = 50 * megabyte;
 // koaMiddleware to manage files
 const homeDir = os.homedir();
 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')
+const gossipPath = path.join(homeDir, '.ssb', 'gossip.json');
+const unfollowedPath = path.join(homeDir, '.ssb', 'gossip_unfollowed.json');
+
+function ensureJSONFile(p, initial = []) {
+  fs.mkdirSync(path.dirname(p), { recursive: true });
+  if (!fs.existsSync(p)) fs.writeFileSync(p, JSON.stringify(initial, null, 2), 'utf8');
+}
 
 function readJSON(p) {
+  ensureJSONFile(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')
+  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`
+  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 core = canonicalKey(key).replace(/^@/, '').replace(/\.ed25519$/, '');
+  return `net:${host}:${Number(port) || 8008}~shs:${core}`;
 }
 
+ensureJSONFile(gossipPath, []);
+ensureJSONFile(unfollowedPath, []);
+
 const koaBodyMiddleware = koaBody({
   multipart: true,
   formidable: {
@@ -980,6 +992,7 @@ router
   .get('/activity', async ctx => {
     const filter = ctx.query.filter || 'recent';
     const userId = SSBconfig.config.keys.id;
+    try { await bankingModel.ensureSelfAddressPublished(); } catch (_) {}
     try { await bankingModel.getUserEngagementScore(userId); } catch (_) {}
     const actions = await activityModel.listFeed(filter);
     ctx.body = activityView(actions, filter, userId);
@@ -1140,8 +1153,8 @@ router
     });
   })
   .get("/peers", async (ctx) => {
-    const onlinePeers = await meta.onlinePeers();
     const { discoveredPeers, unknownPeers } = await meta.discovered();
+    const onlinePeers = await meta.onlinePeers();
     ctx.body = await peersView({
       onlinePeers,
       discoveredPeers,
@@ -2960,16 +2973,13 @@ router
     ctx.redirect("/peers");
   })
   .post("/settings/invite/accept", koaBody(), async (ctx) => {
-   try {
-     const invite = String(ctx.request.body.invite);
-     await meta.acceptInvite(invite);
-   } catch (e) {
-   }
-   ctx.redirect("/invites");
+    const invite = String(ctx.request.body.invite);
+    await meta.acceptInvite(invite);
+    ctx.redirect("/invites");
   })
-  .post('/settings/invite/unfollow', async (ctx) => {
-    const { key } = ctx.request.body || {}
-    if (!key) { ctx.redirect('/invites'); return }
+  .post("/settings/invite/unfollow", koaBody(), 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;
@@ -2981,12 +2991,12 @@ router
     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 { await new Promise(res => ssb.conn.disconnect(addr, res)); } catch {}
+     try { ssb.conn.forget(addr); } catch {}
+    } 
     try {
       await new Promise((resolve, reject) => {
-        ssb.publish({ type: 'contact', contact: canonicalKey(key), following: false, blocking: true }, (err) => err ? reject(err) : resolve());
+        ssb.publish({ type: "contact", contact: canonicalKey(key), following: false, blocking: true }, (err) => err ? reject(err) : resolve());
       });
     } catch {}
     const unf = readJSON(unfollowedPath);
@@ -2997,18 +3007,18 @@ router
       unf.push({ key: canonicalKey(key) });
       writeJSON(unfollowedPath, unf);
     }
-    ctx.redirect('/invites');
-   })
-  .post('/settings/invite/follow', async (ctx) => {
+    ctx.redirect("/invites");
+  })
+  .post("/settings/invite/follow", koaBody(), async (ctx) => {
     const { key, host, port } = ctx.request.body || {};
-    if (!key || !host) { ctx.redirect('/invites'); return; }
+    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');
+      ctx.redirect("/invites");
       return;
     }
     const ssb = await cooler.open();
@@ -3022,16 +3032,16 @@ router
       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 { 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());
-       });
+        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');
+    ctx.redirect("/invites");
   })
   .post("/settings/ssb-logstream", koaBody(), async (ctx) => {
     const logLimit = parseInt(ctx.request.body.ssb_log_limit, 10);
@@ -3139,8 +3149,8 @@ router
     if (pass) currentConfig.wallet.pass = pass;
     if (fee) currentConfig.wallet.fee = fee;
     saveConfig(currentConfig);
-    const referer = new URL(ctx.request.header.referer);
-    ctx.redirect(referer.href);
+    const res = await bankingModel.ensureSelfAddressPublished();
+    ctx.redirect(`/banking?filter=addresses&msg=${encodeURIComponent(res.status)}`);
   })
   .post("/wallet/send", koaBody(), async (ctx) => {
     const action = String(ctx.request.body.action);
@@ -3149,13 +3159,11 @@ router
     const fee = Number(ctx.request.body.fee);
     const { url, user, pass } = getConfig().wallet;
     let balance = null
-
     try {
       balance = await walletModel.getBalance(url, user, pass);
     } catch (error) {
       ctx.body = await walletErrorView(error);
     }
-
     switch (action) {
       case 'confirm':
         const validation = await walletModel.validateSend(url, user, pass, destination, amount, fee);

+ 1 - 1
src/configs/config-manager.js

@@ -58,7 +58,7 @@ if (!fs.existsSync(configFilePath)) {
       "prompt": "Provide an informative and precise response."
     },
     "ssbLogStream": {
-      "limit": 1000
+      "limit": 2000
     }
   };
   fs.writeFileSync(configFilePath, JSON.stringify(defaultConfig, null, 2));

+ 2 - 2
src/configs/oasis-config.json

@@ -41,7 +41,7 @@
     "url": "http://localhost:7474",
     "user": "",
     "pass": "",
-    "fee": "1"
+    "fee": ""
   },
   "walletPub": {
     "url": "",
@@ -52,6 +52,6 @@
     "prompt": "Provide an informative and precise response."
   },
   "ssbLogStream": {
-    "limit": 1000
+    "limit": 2000
   }
 }

+ 14 - 29
src/configs/server-config.json

@@ -3,7 +3,7 @@
     "level": "notice"
   },
   "caps": {
-    "shs": "1KHLiKZvAvjbY1ziZEHMXawbCEIM6qwjCDm3VYRan/s="
+    "shs": "iKOzhqNVTcKEZvUhW3A7TuKZ1d6qIbtsGIJ6+SBOaEQ="
   },
   "pub": false,
   "local": true,
@@ -11,13 +11,6 @@
     "dunbar": 300,
     "hops": 2
   },
-  "autofollow": {
-    "legacy": false,
-    "enabled": false,
-    "feeds": [
-      "@HzmUrdZb1vRWCwn3giLx3p/EWKuDiO44gXAaeulz3d4=.ed25519"
-    ]
-  },
   "gossip": {
     "connections": 20,
     "local": true,
@@ -29,9 +22,13 @@
     "autostart": true,
     "partialReplication": null
   },
+  "autofollow": {
+    "enabled": false,
+    "feeds": []
+  },
   "connections": {
     "seeds": [
-      "solarnethub.com:8008~shs:HzmUrdZb1vRWCwn3giLx3p/EWKuDiO44gXAaeulz3d4=.ed25519"
+      "net:solarnethub.com:8008~shs:HzmUrdZb1vRWCwn3giLx3p/EWKuDiO44gXAaeulz3d4=.ed25519"
     ],
     "incoming": {
       "net": [
@@ -43,7 +40,11 @@
       ],
       "unix": [
         {
-          "scope": ["device", "local", "private"],
+          "scope": [
+            "device",
+            "local",
+            "private"
+          ],
           "transform": "noauth"
         }
       ]
@@ -54,25 +55,9 @@
           "transform": "shs"
         }
       ],
-      "tunnel": [
-        {
-          "scope": "public",
-          "portal": "@HzmUrdZb1vRWCwn3giLx3p/EWKuDiO44gXAaeulz3d4=.ed25519",
-          "transform": "shs"
-        }
-      ],
-      "onion": [
-        {
-          "scope": "public",
-          "transform": "shs"
-        }
-      ],
-      "ws": [
-        {
-          "scope": "public",
-          "transform": "shs"
-        }
-      ]
+      "tunnel": [],
+      "onion": [],
+      "ws": []
     }
   }
 }

+ 1 - 1
src/configs/wallet-addresses.json

@@ -1 +1 @@
-{}
+{}

+ 2 - 1
src/models/activity_model.js

@@ -43,7 +43,8 @@ module.exports = ({ cooler }) => {
         const c = v?.content;
         if (!c?.type) continue;
         if (c.type === 'tombstone' && c.target) { tombstoned.add(c.target); continue }
-        idToAction.set(k, { id: k, author: v?.author, ts: v?.timestamp || 0, type: inferType(c), content: c });
+        const ts = v?.timestamp || Number(c?.timestamp || 0) || (c?.updatedAt ? Date.parse(c.updatedAt) : 0) || 0;
+        idToAction.set(k, { id: k, author: v?.author, ts, type: inferType(c), content: c });
         rawById.set(k, msg);
         if (c.replaces) parentOf.set(k, c.replaces);
       }

+ 65 - 22
src/models/banking_model.js

@@ -40,6 +40,27 @@ function epochIdNow() {
   return `${yyyy}-${String(weekNo).padStart(2, "0")}`;
 }
 
+async function getAnyWalletAddress() {
+  const tryOne = async (method, params = []) => {
+    const r = await rpcCall(method, params, "user");
+    if (!r) return null;
+    if (typeof r === "string" && isValidEcoinAddress(r)) return r;
+    if (Array.isArray(r) && r.length && isValidEcoinAddress(r[0])) return r[0];
+    if (r && typeof r === "object") {
+      const keys = Object.keys(r);
+      if (keys.length && isValidEcoinAddress(keys[0])) return keys[0];
+      if (r.address && isValidEcoinAddress(r.address)) return r.address;
+    }
+    return null;
+  };
+  return await tryOne("getnewaddress")
+      || await tryOne("getaddress")
+      || await tryOne("getaccountaddress", [""])
+      || await tryOne("getaddressesbyaccount", [""])
+      || await tryOne("getaddressesbylabel", [""])
+      || await tryOne("getaddressesbylabel", ["default"]);
+}
+
 async function ensureSelfAddressPublished() {
   const me = config.keys.id;
   const local = readAddrMap();
@@ -47,16 +68,33 @@ async function ensureSelfAddressPublished() {
   if (current && isValidEcoinAddress(current)) return { status: "present", address: current };
   const cfg = getWalletCfg("user") || {};
   if (!cfg.url) return { status: "skipped" };
-  try {
-    const addr = await rpcCall("getaddress", []);
-    if (addr && isValidEcoinAddress(addr)) {
-      await setUserAddress(me, addr, true);
-      return { status: "published", address: addr };
+  const addr = await getAnyWalletAddress();
+  if (addr && isValidEcoinAddress(addr)) {
+    const m = readAddrMap();
+    m[me] = addr;
+    writeAddrMap(m);
+    let ssb = null;
+    try {
+      if (services?.cooler?.open) ssb = await services.cooler.open();
+      else if (global.ssb) ssb = global.ssb;
+      else {
+        try {
+          const srv = require("../server/SSB_server.js");
+          ssb = srv?.ssb || srv?.server || srv?.default || null;
+        } catch (_) {}
+      }
+    } catch (_) {}
+    if (ssb && ssb.publish) {
+      await new Promise((resolve, reject) =>
+	   ssb.publish(
+	      { type: "wallet", coin: "ECO", address: addr, timestamp: Date.now(), updatedAt: new Date().toISOString() },
+	      (err) => err ? reject(err) : resolve()
+	    )
+      );
     }
-  } catch (_) {
-    return { status: "error" };
+    return { status: "published", address: addr };
   }
-  return { status: "noop" };
+  return { status: "error" };
 }
 
 function readJson(p, d) {
@@ -161,16 +199,21 @@ module.exports = ({ services } = {}) => {
     get: async (id) => { ensureStoreFiles(); return readJson(EPOCHS_PATH, []).find(e => e.id === id) || null; }
   };
 
+  let ssbInstance;
   async function openSsb() {
-    if (services?.cooler?.open) return services.cooler.open();
-    if (global.ssb) return global.ssb;
-    try {
-      const srv = require("../server/SSB_server.js");
-      if (srv?.ssb) return srv.ssb;
-      if (srv?.server) return srv.server;
-      if (srv?.default) return srv.default;
-    } catch (_) {}
-    return null; 
+    if (ssbInstance) return ssbInstance;
+    if (services?.cooler?.open) ssbInstance = await services.cooler.open();
+    else if (cooler?.open) ssbInstance = await cooler.open();
+    else if (global.ssb) ssbInstance = global.ssb;
+    else {
+      try {
+        const srv = require("../server/SSB_server.js");
+        ssbInstance = srv?.ssb || srv?.server || srv?.default || null;
+      } catch (_) {
+        ssbInstance = null;
+      }
+    }
+    return ssbInstance;
   }
 
   async function getWalletFromSSB(userId) {
@@ -239,7 +282,7 @@ module.exports = ({ services } = {}) => {
     const m = readAddrMap();
     m[userId] = address;
     writeAddrMap(m);
-    if (publishIfSelf && userId === config.keys.id) await publishSelfAddress(address);
+    if (publishIfSelf && idsEqual(userId, config.keys.id)) await publishSelfAddress(address);
     return true;
   }
 
@@ -247,11 +290,10 @@ module.exports = ({ services } = {}) => {
     if (!userId || !address || !isValidEcoinAddress(address)) return { status: "invalid" };
     const m = readAddrMap();
     const prev = m[userId];
-    if (prev && (prev === address || (prev.address && prev.address === address))) return { status: "exists" };
     m[userId] = address;
     writeAddrMap(m);
-    if (userId === config.keys.id) await publishSelfAddress(address);
-    return { status: prev ? "updated" : "added" };
+    if (idsEqual(userId, config.keys.id)) await publishSelfAddress(address);
+    return { status: prev ? (prev === address || (prev && prev.address === address) ? "exists" : "updated") : "added" };
   }
 
   async function removeAddress({ userId }) {
@@ -649,7 +691,8 @@ async function getLastPublishedTimestamp(userId) {
       userEngagementScore: engagementScore,
       futureUBI
     };
-    return { summary, allocations, epochs, rules: DEFAULT_RULES, addresses };
+    const exchange = await calculateEcoinValue();
+    return { summary, allocations, epochs, rules: DEFAULT_RULES, addresses, exchange };
   }
 
   async function getAllocationById(id) {

+ 1 - 1
src/models/inhabitants_model.js

@@ -85,7 +85,7 @@ module.exports = ({ cooler }) => {
       if (filter === 'all' || filter === 'TOP KARMA') {
         const feedIds = await new Promise((res, rej) => {
           pull(
-            ssbClient.createLogStream({ limit: logLimit }),
+            ssbClient.createLogStream({ limit: logLimit, reverse: true }),
             pull.filter(msg => {
               const c = msg.value?.content;
               const a = msg.value?.author;

+ 38 - 9
src/models/main_models.js

@@ -65,6 +65,7 @@ const configure = (...customOptions) =>
  
 // peers 
 const ebtDir = path.join(os.homedir(), '.ssb', 'ebt');
+const unfollowedPath = path.join(os.homedir(), '.ssb', 'gossip_unfollowed.json');
 
 async function loadPeersFromEbt() {
   let result = [];
@@ -123,6 +124,38 @@ const parseRemote = (remote) => {
   return { host, pubId };
 };
 
+async function ensureJSONFile(p, initial = []) {
+  await fs.mkdir(path.dirname(p), { recursive: true });
+  try { await fs.access(p) } catch { await fs.writeFile(p, JSON.stringify(initial, null, 2), 'utf8') }
+}
+
+async function readJSON(p) {
+  await ensureJSONFile(p, []);
+  try { return JSON.parse((await fs.readFile(p, 'utf8')) || '[]') } catch { return [] }
+}
+
+function canonicalKey(key) {
+  let core = String(key).replace(/^@/, '').replace(/\.ed25519$/, '').replace(/-/g, '+').replace(/_/g, '/');
+  if (!core.endsWith('=')) core += '=';
+  return `@${core}.ed25519`;
+}
+
+async function loadUnfollowedSet() {
+  const list = await readJSON(unfollowedPath);
+  return new Set(list.map(x => canonicalKey(x && x.key)));
+}
+
+function toLegacyInvite(s) {
+  const t = String(s || '').trim();
+  if (/^[^:]+:\d+:@[^~]+~[^~]+$/.test(t)) return t;
+  let m = t.match(/^net:([^:]+):(\d+)~shs:([^~]+)~invite:([^~]+)$/);
+  if (!m) m = t.match(/^([^:]+):(\d+)~shs:([^~]+)~invite:([^~]+)$/);
+  if (!m) return t;
+  let key = m[3].replace(/^@/, '');
+  if (!/\.ed25519$/.test(key)) key += '.ed25519';
+  return `${m[1]}:${m[2]}:@${key}~${m[4]}`;
+}
+
 // core modules
 module.exports = ({ cooler, isPublic }) => {
   const models = {};
@@ -586,14 +619,7 @@ models.meta = {
       for (const { pub } of ebtList) {
         if (!discoveredIds.has(pub)) {
           const name = await models.about.name(pub).catch(() => pub);
-          unknownPeers.push([
-             pub,
-            {
-              key: pub,
-              name,
-              users: ebtMap.get(pub) || []
-            }
-          ]);
+          unknownPeers.push([pub, { key: pub, name, users: ebtMap.get(pub) || [] }]);
         }
       }
       return { discoveredPeers, unknownPeers };
@@ -662,7 +688,10 @@ models.meta = {
     },
     acceptInvite: async (invite) => {
       const ssb = await cooler.open();
-      return await ssb.invite.accept(invite);
+      const code = toLegacyInvite(String(invite || ''));
+      return await new Promise((resolve, reject) => {
+        ssb.invite.accept(code, (err, res) => err ? reject(err) : resolve(res));
+      });
     },
     rebuild: async () => {
       const ssb = await cooler.open();

+ 29 - 8
src/server/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.4.7",
+  "version": "0.4.8",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@krakenslab/oasis",
-      "version": "0.4.0",
+      "version": "0.4.7",
       "hasInstallScript": true,
       "license": "AGPL-3.0",
       "dependencies": {
@@ -86,7 +86,7 @@
         "ssb-conn-staging": "^1.0.0",
         "ssb-db": "^20.4.1",
         "ssb-device-address": "^1.1.6",
-        "ssb-ebt": "^9.0.0",
+        "ssb-ebt": "^9.1.2",
         "ssb-friend-pub": "^1.0.7",
         "ssb-friends": "^5.0.0",
         "ssb-gossip": "^1.1.1",
@@ -19812,16 +19812,23 @@
       }
     },
     "node_modules/sodium-browserify-tweetnacl/node_modules/sha.js": {
-      "version": "2.4.11",
-      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
-      "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
+      "version": "2.4.12",
+      "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
+      "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
       "license": "(MIT AND BSD-3-Clause)",
       "dependencies": {
-        "inherits": "^2.0.1",
-        "safe-buffer": "^5.0.1"
+        "inherits": "^2.0.4",
+        "safe-buffer": "^5.2.1",
+        "to-buffer": "^1.2.0"
       },
       "bin": {
         "sha.js": "bin.js"
+      },
+      "engines": {
+        "node": ">= 0.10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
       }
     },
     "node_modules/sodium-browserify-tweetnacl/node_modules/tweetnacl": {
@@ -23679,6 +23686,20 @@
         "node": ">=0.6.0"
       }
     },
+    "node_modules/to-buffer": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.1.tgz",
+      "integrity": "sha512-tB82LpAIWjhLYbqjx3X4zEeHN6M8CiuOEy2JY8SEQVdYRe3CCHOFaqrBW1doLDrfpWhplcW7BL+bO3/6S3pcDQ==",
+      "license": "MIT",
+      "dependencies": {
+        "isarray": "^2.0.5",
+        "safe-buffer": "^5.2.1",
+        "typed-array-buffer": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
     "node_modules/to-camel-case": {
       "version": "1.0.0",
       "resolved": "https://registry.npmjs.org/to-camel-case/-/to-camel-case-1.0.0.tgz",

+ 2 - 2
src/server/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@krakenslab/oasis",
-  "version": "0.4.7",
+  "version": "0.4.8",
   "description": "Oasis Social Networking Project Utopia",
   "repository": {
     "type": "git",
@@ -100,7 +100,7 @@
     "ssb-conn-staging": "^1.0.0",
     "ssb-db": "^20.4.1",
     "ssb-device-address": "^1.1.6",
-    "ssb-ebt": "^9.0.0",
+    "ssb-ebt": "^9.1.2",
     "ssb-friend-pub": "^1.0.7",
     "ssb-friends": "^5.0.0",
     "ssb-gossip": "^1.1.1",

+ 25 - 19
src/views/banking_views.js

@@ -271,24 +271,30 @@ const renderAddresses = (data, userId) => {
 };
 
 const renderBankingView = (data, filter, userId) =>
-    template(
-        i18n.banking,
-        section(
-            div({ class: "tags-header" }, h2(i18n.banking), p(i18n.bankingDescription)),
-            generateFilterButtons(["overview", "exchange", "mine", "pending", "closed", "epochs", "rules", "addresses"], filter, "/banking"),
-            filter === "overview"
-                ? div(
-                    renderOverviewSummaryTable(data.summary || {}, data.rules),
-                    allocationsTable((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), userId)
-                )
-                : filter === "exchange"
-                    ? renderExchange(data.exchange)
-                    : allocationsTable(
-                        (filterAllocations((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), filter, userId)),
-                        userId
-                    )
-        )
-    );
-
+  template(
+    i18n.banking,
+    section(
+      div({ class: "tags-header" }, h2(i18n.banking), p(i18n.bankingDescription)),
+      generateFilterButtons(["overview","exchange","mine","pending","closed","epochs","rules","addresses"], filter, "/banking"),
+      filter === "overview"
+        ? div(
+            renderOverviewSummaryTable(data.summary || {}, data.rules),
+            allocationsTable((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), userId)
+          )
+        : filter === "exchange"
+        ? renderExchange(data.exchange)
+        : filter === "epochs"
+        ? renderEpochList(data.epochs || [])
+        : filter === "rules"
+        ? rulesBlock(data.rules || {})
+        : filter === "addresses"
+        ? renderAddresses(data, userId)
+        : allocationsTable(
+            filterAllocations((data.allocations || []).sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)), filter, userId),
+            userId
+          )
+    )
+  )
+  
 module.exports = { renderBankingView };
 

+ 32 - 14
src/views/peers_view.js

@@ -1,11 +1,19 @@
 const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers }) => {
   const { form, button, div, h2, p, section, ul, li, a, hr } = require("../server/node_modules/hyperaxe");
   const { template, i18n } = require('./main_views');
+
   const startButton = form({ action: "/settings/conn/start", method: "post" }, button({ type: "submit" }, i18n.startNetworking));
   const restartButton = form({ action: "/settings/conn/restart", method: "post" }, button({ type: "submit" }, i18n.restartNetworking));
   const stopButton = form({ action: "/settings/conn/stop", method: "post" }, button({ type: "submit" }, i18n.stopNetworking));
   const syncButton = form({ action: "/settings/conn/sync", method: "post" }, button({ type: "submit" }, i18n.sync));
   const connButtons = [startButton, restartButton, stopButton, syncButton];
+
+  const encodePubKey = (pubId) => {
+    let core = pubId.replace(/^@/, '').replace(/\.ed25519$/, '').replace(/_/g, '/');
+    if (!core.endsWith('=')) core += '=';
+    return `/author/${encodeURIComponent('@' + core)}.ed25519`;
+  };
+
   const renderInhabitants = (users, pubID) => {
     const filteredUsers = users.filter(user => user.id !== pubID);
     if (filteredUsers.length === 0) {
@@ -18,22 +26,32 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers }) => {
       );
     });
   };
-  const encodePubKey = (pubId) => {
-    let core = pubId.replace(/^@/, '').replace(/\.ed25519$/, '').replace(/_/g, '/');
-    if (!core.endsWith('=')) core += '=';
-    return `/author/${encodeURIComponent('@' + core)}.ed25519`;
-  };
+
   const renderPeer = (peerData) => {
-  const peer = peerData[1];
-  const { name, users, key } = peer;
-  const pubUrl = encodePubKey(key);
-  const inhabitants = renderInhabitants(users, peerData[0]);
+    const peer = peerData[1];
+    const { name, users, key } = peer;
+    const pubUrl = encodePubKey(key);
+    const inhabitants = renderInhabitants(users, peerData[0]);
     return li(
       `${i18n.pub}: ${name} `,
       a({ href: pubUrl, class:"user-link" }, `${key}`),
       inhabitants.length > 0 ? ul(inhabitants) : p(i18n.noDiscovered)
     );
   };
+
+  const countPeers = (list) => {
+    let usersTotal = 0;
+    for (const item of list) {
+      const users = (item[1].users || []).filter(u => u.id !== item[0]);
+      usersTotal += users.length;
+    }
+    return list.length + usersTotal;
+  };
+
+  const onlineCount = countPeers(onlinePeers);
+  const discoveredCount = countPeers(discoveredPeers);
+  const unknownCount = countPeers(unknownPeers);
+
   return template(
     i18n.peers,
     section(
@@ -43,13 +61,13 @@ const peersView = async ({ onlinePeers, discoveredPeers, unknownPeers }) => {
       ),
       div({ class: "conn-actions" }, ...connButtons),
       div({ class: "peers-list" },
-        h2(i18n.online),
+        h2(`${i18n.online} (${onlineCount})`),
         onlinePeers.length > 0 ? ul(onlinePeers.map(renderPeer)) : p(i18n.noConnections),
-        hr,
-        h2(i18n.discovered),
+        hr(),
+        h2(`${i18n.discovered} (${discoveredCount})`),
         discoveredPeers.length > 0 ? ul(discoveredPeers.map(renderPeer)) : p(i18n.noDiscovered),
-        hr,
-        h2(i18n.unknown),
+        hr(),
+        h2(`${i18n.unknown} (${unknownCount})`),
         unknownPeers.length > 0 ? ul(unknownPeers.map(renderPeer)) : p(i18n.noDiscovered),
         p(i18n.connectionActionIntro)
       )

+ 1 - 1
src/views/settings_view.js

@@ -24,7 +24,7 @@ const settingsView = ({ version, aiPrompt }) => {
   const currentConfig = getConfig();
   const walletUrl = currentConfig.wallet.url;
   const walletUser = currentConfig.wallet.user;
-  const walletFee = currentConfig.wallet.feee;
+  const walletFee = currentConfig.wallet.fee;
   const pubWalletUrl = currentConfig.walletPub.url || '';
   const pubWalletUser = currentConfig.walletPub.user || '';
   const pubWalletPass = currentConfig.walletPub.pass || '';