upstream: integrar Oasis 1.3.1
Base del merge a tres bandas: git 0.9.5 de epsylon, esta rama y el src de la
APK oficial 1.3.1 (la variante movil: OASIS_MOBILE, regex sin \p{}, audio y
video de Android). Corre en el mismo nodejs-mobile 18.20.4 que ya usabamos.
Se conserva de la rama: Karvan, identidades y multicuenta, topbar y barra
inferior, borrado de EXIF sin sharp, filtro hot del foro, RTC sin STUN y los
modulos apagados en movil (wallet, banking, torrents).
Se toma de upstream lo que ya hace de serie: respuesta citada en chats, ssb-lan
en movil, configuracion en ~/.ssb/oasis (sobrevive a reinstalar el backend).
El asistente de bienvenida deja elegir workflow tambien en movil.
This commit is contained in:
parent
8d838dd8b1
commit
c332afd6ea
265 changed files with 80970 additions and 29167 deletions
|
|
@ -2,6 +2,8 @@ const pull = require('../server/node_modules/pull-stream');
|
|||
const config = require('../server/ssb_config');
|
||||
const { getConfig } = require('../configs/config-manager.js');
|
||||
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
|
||||
const { readContentWindow, requestScope, logTip } = require('./typed_log');
|
||||
const { versionGroups, versionEntry } = require('../backend/dedupe');
|
||||
const logLimit = getConfig().ssbLogStream?.limit || 1000;
|
||||
|
||||
module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
||||
|
|
@ -11,6 +13,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
|||
if (!ssb) ssb = await cooler.open();
|
||||
return ssb;
|
||||
};
|
||||
const nameByFeedId = new Map();
|
||||
|
||||
const HIDDEN_ENVELOPE_TYPES = new Set([
|
||||
'tribe-keys-distrib',
|
||||
|
|
@ -19,7 +22,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
|||
]);
|
||||
|
||||
const isHiddenBoxedContent = (rawContent) =>
|
||||
typeof rawContent === 'string' && rawContent.endsWith('.box');
|
||||
typeof rawContent === 'string' && /\.box\d*$/.test(rawContent);
|
||||
|
||||
const buildAccessibleTribeIds = async () => {
|
||||
const set = new Set();
|
||||
|
|
@ -41,16 +44,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
|||
const hasBlob = async (ssbClient, url) =>
|
||||
new Promise(resolve => ssbClient.blobs.has(url, (err, has) => resolve(!err && has)));
|
||||
|
||||
const isClosedSold = s =>
|
||||
String(s || '').toUpperCase() === 'SOLD' || String(s || '').toUpperCase() === 'CLOSED';
|
||||
|
||||
const projectRank = (status) => {
|
||||
const S = String(status || '').toUpperCase();
|
||||
if (S === 'COMPLETED') return 3;
|
||||
if (S === 'ACTIVE') return 2;
|
||||
if (S === 'PAUSED') return 1;
|
||||
if (S === 'CANCELLED') return 0;
|
||||
return -1;
|
||||
const isReplacedBlock = (vg, tombstoned, block) => {
|
||||
const c = block.content;
|
||||
const rootDeleted = !c.replaces && c?.type === 'forum' && c.root && tombstoned.has(c.root);
|
||||
return vg.tipOf(block.id) !== block.id || vg.isForeignCopy(block.id) || tombstoned.has(block.id) || !!rootDeleted;
|
||||
};
|
||||
|
||||
const safeDecode = (s) => {
|
||||
|
|
@ -127,115 +124,83 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
|||
return nameByFeedId;
|
||||
};
|
||||
|
||||
let windowCache = null;
|
||||
const readWindow = async (ssbClient) => {
|
||||
const tip = await logTip(ssbClient).catch(() => null);
|
||||
if (tip && windowCache && windowCache.tip === tip) return windowCache.msgs;
|
||||
const msgs = await readContentWindow(ssbClient, logLimit);
|
||||
if (tip) windowCache = { tip, msgs };
|
||||
return msgs;
|
||||
};
|
||||
|
||||
async function buildBlocks(showLogs, me) {
|
||||
const ssbClient = await openSsb();
|
||||
|
||||
const results = await readWindow(ssbClient);
|
||||
|
||||
const tombstoned = buildValidatedTombstoneSet(results);
|
||||
const idToBlock = new Map();
|
||||
const versions = [];
|
||||
|
||||
const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null;
|
||||
const accessibleTribeIds = await buildAccessibleTribeIds();
|
||||
for (const msg of results) {
|
||||
const k = msg.key;
|
||||
let c = msg.value?.content;
|
||||
const author = msg.value?.author;
|
||||
if (isHiddenBoxedContent(c) || msg.value?.private === true) continue;
|
||||
if (showLogs && typeof c === 'string' && author === me) {
|
||||
try {
|
||||
const dec = ssbClient.private.unbox({ key: k, value: msg.value, timestamp: msg.timestamp || msg.value?.timestamp || 0 });
|
||||
c = dec?.value?.content;
|
||||
} catch { c = null; }
|
||||
}
|
||||
if (!c?.type) continue;
|
||||
if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue;
|
||||
if (tribeCrypto && tribeCrypto.isTribeMsg(c)) {
|
||||
const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null;
|
||||
if (!r || !r.body) continue;
|
||||
const inner = r.body;
|
||||
const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg');
|
||||
c = { type: innerType, _decrypted: true, _rootId: r.rootId, ...inner };
|
||||
} else if (c.tribeId && !accessibleTribeIds.has(c.tribeId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c.type === 'about') {
|
||||
const aboutId = String(c.about || author || '').trim();
|
||||
const nm = typeof c.name === 'string' ? c.name.trim() : '';
|
||||
if (aboutId && nm && !nameByFeedId.has(aboutId)) nameByFeedId.set(aboutId, nm);
|
||||
}
|
||||
|
||||
if (c.type === 'tombstone' && c.target) {
|
||||
idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') });
|
||||
continue;
|
||||
}
|
||||
versions.push(versionEntry({ key: k, value: { ...msg.value, content: c } }));
|
||||
idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') });
|
||||
}
|
||||
|
||||
const vg = versionGroups(versions);
|
||||
|
||||
const blockData = Array.from(idToBlock.values()).map(block => ({
|
||||
...block,
|
||||
isTombstoned: tombstoned.has(block.id),
|
||||
isReplaced: isReplacedBlock(vg, tombstoned, block)
|
||||
}));
|
||||
|
||||
return blockData;
|
||||
}
|
||||
|
||||
return {
|
||||
async listBlockchain(filter = 'all', userId, search = {}) {
|
||||
const ssbClient = await openSsb();
|
||||
|
||||
const results = await new Promise((resolve, reject) =>
|
||||
pull(
|
||||
ssbClient.createLogStream({ reverse: true, limit: logLimit }),
|
||||
pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))
|
||||
)
|
||||
);
|
||||
|
||||
const tombstoned = buildValidatedTombstoneSet(results);
|
||||
const idToBlock = new Map();
|
||||
const referencedAsReplaces = new Set();
|
||||
|
||||
const nameByFeedId = new Map();
|
||||
|
||||
const showLogs = (filter === 'logs' || filter === 'LOGS');
|
||||
const me = userId || config.keys.id;
|
||||
const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null;
|
||||
const accessibleTribeIds = await buildAccessibleTribeIds();
|
||||
for (const msg of results) {
|
||||
const k = msg.key;
|
||||
let c = msg.value?.content;
|
||||
const author = msg.value?.author;
|
||||
if (isHiddenBoxedContent(c)) continue;
|
||||
if (showLogs && typeof c === 'string' && author === me) {
|
||||
try {
|
||||
const dec = ssbClient.private.unbox({ key: k, value: msg.value, timestamp: msg.timestamp || msg.value?.timestamp || 0 });
|
||||
c = dec?.value?.content;
|
||||
} catch { c = null; }
|
||||
}
|
||||
if (!c?.type) continue;
|
||||
if (HIDDEN_ENVELOPE_TYPES.has(c.type)) continue;
|
||||
if (tribeCrypto && tribeCrypto.isTribeMsg(c)) {
|
||||
const r = fpIdx ? tribeCrypto.unwrapMsg(c, fpIdx) : null;
|
||||
if (!r || !r.body) continue;
|
||||
const inner = r.body;
|
||||
const innerType = inner.k === 'tribe' ? 'tribe' : (inner.k === 'tribe-content' ? `tribe-content:${inner.contentType || ''}` : inner.k || 'tribe-msg');
|
||||
c = { type: innerType, _decrypted: true, _rootId: r.rootId, ...inner };
|
||||
} else if (c.tribeId && !accessibleTribeIds.has(c.tribeId)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c.type === 'about') {
|
||||
const aboutId = String(c.about || author || '').trim();
|
||||
const nm = typeof c.name === 'string' ? c.name.trim() : '';
|
||||
if (aboutId && nm && !nameByFeedId.has(aboutId)) nameByFeedId.set(aboutId, nm);
|
||||
}
|
||||
|
||||
if (c.type === 'tombstone' && c.target) {
|
||||
idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') });
|
||||
continue;
|
||||
}
|
||||
if (c.replaces) referencedAsReplaces.add(c.replaces);
|
||||
idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') });
|
||||
}
|
||||
|
||||
const tipBlocks = [];
|
||||
for (const [id, block] of idToBlock.entries()) {
|
||||
if (!referencedAsReplaces.has(id) && block.content.replaces) tipBlocks.push(block);
|
||||
}
|
||||
for (const [id, block] of idToBlock.entries()) {
|
||||
if (!block.content.replaces && !referencedAsReplaces.has(id)) tipBlocks.push(block);
|
||||
}
|
||||
|
||||
const groups = {};
|
||||
for (const block of tipBlocks) {
|
||||
const ancestor = block.content.replaces || block.id;
|
||||
if (!groups[ancestor]) groups[ancestor] = [];
|
||||
groups[ancestor].push(block);
|
||||
}
|
||||
|
||||
const liveTipIds = new Set();
|
||||
for (const groupBlocks of Object.values(groups)) {
|
||||
let best = groupBlocks[0];
|
||||
for (const block of groupBlocks) {
|
||||
if (block.type === 'market') {
|
||||
if (isClosedSold(block.content.status) && !isClosedSold(best.content.status)) {
|
||||
best = block;
|
||||
} else if ((block.content.status === best.content.status) && block.ts > best.ts) {
|
||||
best = block;
|
||||
}
|
||||
} else if (block.type === 'project') {
|
||||
const br = projectRank(best.content.status);
|
||||
const cr = projectRank(block.content.status);
|
||||
if (cr > br || (cr === br && block.ts > best.ts)) best = block;
|
||||
} else if (block.type === 'job' || block.type === 'forum') {
|
||||
if (block.ts > best.ts) best = block;
|
||||
} else {
|
||||
if (block.ts > best.ts) best = block;
|
||||
}
|
||||
}
|
||||
liveTipIds.add(best.id);
|
||||
}
|
||||
|
||||
const blockData = Array.from(idToBlock.values()).map(block => {
|
||||
const c = block.content;
|
||||
const rootDeleted = c?.type === 'forum' && c.root && tombstoned.has(c.root);
|
||||
return {
|
||||
...block,
|
||||
isTombstoned: tombstoned.has(block.id),
|
||||
isReplaced: c.replaces
|
||||
? (!liveTipIds.has(block.id) || tombstoned.has(block.id))
|
||||
: referencedAsReplaces.has(block.id) || tombstoned.has(block.id) || rootDeleted
|
||||
};
|
||||
});
|
||||
|
||||
let filtered = blockData;
|
||||
const store = requestScope.getStore();
|
||||
const memoKey = `blockchainBase:${showLogs}:${me}`;
|
||||
if (store && !store[memoKey]) store[memoKey] = buildBlocks(showLogs, me);
|
||||
const blockData = await (store ? store[memoKey] : buildBlocks(showLogs, me));
|
||||
let filtered = blockData.slice();
|
||||
|
||||
if (filter === 'RECENT' || filter === 'recent') {
|
||||
const now = Date.now();
|
||||
|
|
@ -267,7 +232,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
|||
const needsNameSearch = !!authorQ && !authorQ.toLowerCase().includes('.ed25519');
|
||||
|
||||
if (needsNameSearch) {
|
||||
aboutIndex = await buildNameIndexFromAbout(ssbClient, 10000);
|
||||
aboutIndex = await buildNameIndexFromAbout(await openSsb(), 10000);
|
||||
for (const [fid, nm] of aboutIndex.entries()) {
|
||||
if (!nameByFeedId.has(fid)) nameByFeedId.set(fid, nm);
|
||||
}
|
||||
|
|
@ -293,17 +258,12 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
|||
|
||||
async getBlockById(id, userId) {
|
||||
const ssbClient = await openSsb();
|
||||
const results = await new Promise((resolve, reject) =>
|
||||
pull(
|
||||
ssbClient.createLogStream({ reverse: true, limit: logLimit }),
|
||||
pull.collect((err, msgs) => err ? reject(err) : resolve(msgs))
|
||||
)
|
||||
);
|
||||
const results = await readWindow(ssbClient);
|
||||
|
||||
const me = userId || config.keys.id;
|
||||
const tombstoned = buildValidatedTombstoneSet(results);
|
||||
const idToBlock = new Map();
|
||||
const referencedAsReplaces = new Set();
|
||||
const versions = [];
|
||||
const fpIdx = tribeCrypto ? tribeCrypto.buildFingerprintIndex() : null;
|
||||
const accessibleTribeIds = await buildAccessibleTribeIds();
|
||||
|
||||
|
|
@ -311,7 +271,7 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
|||
const k = msg.key;
|
||||
let c = msg.value?.content;
|
||||
const author = msg.value?.author;
|
||||
if (isHiddenBoxedContent(c)) continue;
|
||||
if (isHiddenBoxedContent(c) || msg.value?.private === true) continue;
|
||||
if (typeof c === 'string' && author === me) {
|
||||
try {
|
||||
const dec = ssbClient.private.unbox({ key: k, value: msg.value, timestamp: msg.timestamp || msg.value?.timestamp || 0 });
|
||||
|
|
@ -333,48 +293,10 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
|||
idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') });
|
||||
continue;
|
||||
}
|
||||
if (c.replaces) referencedAsReplaces.add(c.replaces);
|
||||
versions.push(versionEntry({ key: k, value: { ...msg.value, content: c } }));
|
||||
idToBlock.set(k, { id: k, author, ts: msg.value.timestamp, type: c.type, content: c, size: Buffer.byteLength(JSON.stringify(msg.value), 'utf8') });
|
||||
}
|
||||
|
||||
const tipBlocks = [];
|
||||
for (const [bid, block] of idToBlock.entries()) {
|
||||
if (!referencedAsReplaces.has(bid) && block.content.replaces) tipBlocks.push(block);
|
||||
}
|
||||
for (const [bid, block] of idToBlock.entries()) {
|
||||
if (!block.content.replaces && !referencedAsReplaces.has(bid)) tipBlocks.push(block);
|
||||
}
|
||||
|
||||
const groups = {};
|
||||
for (const block of tipBlocks) {
|
||||
const ancestor = block.content.replaces || block.id;
|
||||
if (!groups[ancestor]) groups[ancestor] = [];
|
||||
groups[ancestor].push(block);
|
||||
}
|
||||
|
||||
const liveTipIds = new Set();
|
||||
for (const groupBlocks of Object.values(groups)) {
|
||||
let best = groupBlocks[0];
|
||||
for (const block of groupBlocks) {
|
||||
if (block.type === 'market') {
|
||||
if (isClosedSold(block.content.status) && !isClosedSold(best.content.status)) {
|
||||
best = block;
|
||||
} else if ((block.content.status === best.content.status) && block.ts > best.ts) {
|
||||
best = block;
|
||||
}
|
||||
} else if (block.type === 'project') {
|
||||
const br = projectRank(best.content.status);
|
||||
const cr = projectRank(block.content.status);
|
||||
if (cr > br || (cr === br && block.ts > best.ts)) best = block;
|
||||
} else if (block.type === 'job' || block.type === 'forum') {
|
||||
if (block.ts > best.ts) best = block;
|
||||
} else {
|
||||
if (block.ts > best.ts) best = block;
|
||||
}
|
||||
}
|
||||
liveTipIds.add(best.id);
|
||||
}
|
||||
|
||||
const block = idToBlock.get(id);
|
||||
if (!block) return null;
|
||||
|
||||
|
|
@ -383,12 +305,8 @@ module.exports = ({ cooler, tribeCrypto, tribesModel }) => {
|
|||
if (!valid) return null;
|
||||
}
|
||||
|
||||
const c = block.content;
|
||||
const rootDeleted = c?.type === 'forum' && c.root && tombstoned.has(c.root);
|
||||
const isTombstoned = tombstoned.has(block.id);
|
||||
const isReplaced = c.replaces
|
||||
? (!liveTipIds.has(block.id) || tombstoned.has(block.id))
|
||||
: referencedAsReplaces.has(block.id) || tombstoned.has(block.id) || rootDeleted;
|
||||
const isReplaced = isReplacedBlock(versionGroups(versions), tombstoned, block);
|
||||
|
||||
return { ...block, isTombstoned, isReplaced };
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue