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:
oasis-bot 2026-10-18 02:52:17 +02:00
parent 8d838dd8b1
commit c332afd6ea
265 changed files with 80970 additions and 29167 deletions

View file

@ -3,7 +3,8 @@ const moment = require("../server/node_modules/moment")
const { getConfig } = require("../configs/config-manager.js")
const categories = require("../backend/opinion_categories")
const { buildValidatedTombstoneSet } = require('./tombstone_validator')
const { dedupeByPreferring, norm } = require('../backend/dedupe')
const { readTyped, memoIndex } = require('./typed_log')
const { dedupeByPreferring, norm, versionGroups } = require('../backend/dedupe')
const logLimit = getConfig().ssbLogStream?.limit || 1000
const N = (s) => String(s || "").toUpperCase().replace(/\s+/g, "_")
@ -63,11 +64,9 @@ module.exports = ({ cooler, tribeCrypto }) => {
return ssb
}
const readAll = async (ssbClient) => {
return new Promise((resolve, reject) =>
pull(ssbClient.createLogStream({ limit: logLimit }), pull.collect((err, msgs) => (err ? reject(err) : resolve(msgs))))
)
}
const MARKET_TYPES = ["market", "marketBid", "marketPurchase", "marketOpinion", "tombstone"]
const readAll = async (ssbClient) => readTyped(ssbClient, MARKET_TYPES, { limit: logLimit })
const buildMarketIndex = (messages) => {
const tomb = buildValidatedTombstoneSet(messages)
@ -83,31 +82,16 @@ module.exports = ({ cooler, tribeCrypto }) => {
if (c.type === "marketPurchase") { purchases.push({ target: c.target, author: m.value.author, ts: (m.value && m.value.timestamp) || 0 }); continue }
if (c.type === "marketOpinion" && c.target) { opinionMsgs.push({ target: c.target, author: m.value.author, category: c.category }); continue }
if (c.type !== "market") continue
nodes.set(m.key, { key: m.key, ts: (m.value && m.value.timestamp) || m.timestamp || 0, c, author: m.value.author })
nodes.set(m.key, { key: m.key, ts: (m.value && m.value.timestamp) || m.timestamp || 0, seq: m.value.sequence || 0, c, author: m.value.author })
}
const naivePrev = new Map(), strictNext = new Map()
for (const [key, n] of nodes) {
const t = n.c.replaces
if (!t) continue
naivePrev.set(key, t)
}
for (const [child, parent] of Array.from(naivePrev.entries())) {
const cn = nodes.get(child)
const pn = nodes.get(parent)
if (!pn) { naivePrev.delete(child); continue }
if (!cn || cn.author !== pn.author) { naivePrev.delete(child); nodes.delete(child); continue }
strictNext.set(parent, child)
}
return { tomb, nodes, bids, purchases, opinionMsgs, naivePrev, strictNext }
const vg = versionGroups(Array.from(nodes.values()).map(n => ({ key: n.key, author: n.author, seq: n.seq, ts: n.ts, replaces: typeof n.c.replaces === "string" ? n.c.replaces : null, claimedAuthor: typeof n.c.seller === "string" ? n.c.seller : null })))
return { tomb, nodes, bids, purchases, opinionMsgs, vg }
}
const resolveGroups = (idx) => {
const { tomb, nodes, bids, purchases, opinionMsgs, naivePrev, strictNext } = idx
const rootOf = (key) => { let x = key, g = 0; while (naivePrev.has(x) && nodes.has(naivePrev.get(x)) && g++ < 100000) x = naivePrev.get(x); return x }
const followStrict = (key) => { let x = key, g = 0; while (strictNext.has(x) && g++ < 100000) x = strictNext.get(x); return x }
const { tomb, nodes, bids, purchases, opinionMsgs, vg } = idx
const rootOf = vg.rootOf
const groups = new Map()
for (const key of nodes.keys()) { const r = rootOf(key); if (!groups.has(r)) groups.set(r, []); groups.get(r).push(key) }
const bidsByRoot = new Map()
for (const bd of bids) { if (!nodes.has(bd.target)) continue; const r = rootOf(bd.target); if (!bidsByRoot.has(r)) bidsByRoot.set(r, []); bidsByRoot.get(r).push(bd) }
const soldByRoot = new Map()
@ -128,13 +112,12 @@ module.exports = ({ cooler, tribeCrypto }) => {
}
const out = new Map()
for (const [root, keys] of groups) {
for (const root of vg.roots) {
const keys = vg.groups.get(root) || [root]
const rootNode = nodes.get(root)
const sellerId = rootNode ? rootNode.author : null
const sellerKeys = keys.filter(k => { const n = nodes.get(k); return n && n.author === sellerId })
let tip = followStrict(root)
const tipNode0 = nodes.get(tip)
if (!tipNode0 || tipNode0.author !== sellerId) tip = root
const tip = vg.tipOf(root)
if (tomb.has(tip)) continue
let best = nodes.get(tip)
if (!best) continue
@ -225,10 +208,9 @@ module.exports = ({ cooler, tribeCrypto }) => {
async resolveCurrentId(itemId) {
const ssbClient = await openSsb()
const messages = await readAll(ssbClient)
const { tomb, strictNext } = buildMarketIndex(messages)
let cur = itemId, g = 0
while (strictNext.has(cur) && g++ < 100000) cur = strictNext.get(cur)
if (tomb.has(cur)) throw new Error("Item not found")
const { tomb, vg } = buildMarketIndex(messages)
const cur = vg.tipOf(itemId)
if (tomb.has(cur) || vg.isForeignCopy(vg.rootOf(cur))) throw new Error("Item not found")
return cur
},
@ -431,9 +413,8 @@ module.exports = ({ cooler, tribeCrypto }) => {
const userId = ssbClient.id
const messages = await readAll(ssbClient)
const idx = buildMarketIndex(messages)
const rootOf = (key) => { let x = key, g = 0; while (idx.naivePrev.has(x) && idx.nodes.has(idx.naivePrev.get(x)) && g++ < 100000) x = idx.naivePrev.get(x); return x }
const grp = resolveGroups(idx).get(rootOf(itemId))
const idx = memoIndex('market', messages, buildMarketIndex)
const grp = resolveGroups(idx).get(idx.vg.rootOf(itemId))
if (!grp) return null
const { tip, rootId, best, statusN, poll, soldCount, buyers, opinions, voters } = grp
@ -495,6 +476,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
const myId = ssbClient.id
const now = moment()
const list = Array.isArray(items) ? items : []
let changed = 0
for (const item of list) {
if (!item || !item.deadline) continue
@ -517,8 +499,10 @@ module.exports = ({ cooler, tribeCrypto }) => {
try {
await this.updateItemById(item.id, { status })
changed++
} catch (_) {}
}
return changed
},
async getItemByShopProductId(shopProductId) {
@ -532,11 +516,8 @@ module.exports = ({ cooler, tribeCrypto }) => {
const ssbClient = await openSsb()
const userId = ssbClient.id
const messages = await readAll(ssbClient)
const groups = resolveGroups(buildMarketIndex(messages))
let group = null
for (const g of groups.values()) {
if (g.rootId === itemId || g.tip === itemId) { group = g; break }
}
const idx = buildMarketIndex(messages)
const group = resolveGroups(idx).get(idx.vg.rootOf(itemId))
if (!group) throw new Error("Item not found")
if (group.best.author === userId) throw new Error("You cannot rate your own item")
if ((group.voters || []).includes(userId)) throw new Error("Already voted")
@ -595,8 +576,7 @@ module.exports = ({ cooler, tribeCrypto }) => {
const ssbClient = await openSsb()
const messages = await readAll(ssbClient)
const idx = buildMarketIndex(messages)
const rootOf = (key) => { let x = key, g = 0; while (idx.naivePrev.has(x) && idx.nodes.has(idx.naivePrev.get(x)) && g++ < 100000) x = idx.naivePrev.get(x); return x }
const grp = resolveGroups(idx).get(rootOf(itemId))
const grp = resolveGroups(idx).get(idx.vg.rootOf(itemId))
if (!grp) throw new Error("Item not found")
const c = grp.best.c