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

@ -1,8 +1,49 @@
const pull = require('../server/node_modules/pull-stream');
const { getConfig } = require('../configs/config-manager.js');
const { buildValidatedTombstoneSet } = require('./tombstone_validator');
const { readTyped } = require('./typed_log');
const logLimit = getConfig().ssbLogStream?.limit || 1000;
const PIXELIA_TYPES = ['pixelia', 'tombstone'];
const COLOR_RE = /^#[0-9a-fA-F]{6}$/;
const pixelCoordinate = (c) => {
if (!Number.isInteger(c.x) || !Number.isInteger(c.y) || c.x < 0 || c.x > 49 || c.y < 0 || c.y > 199) return null;
const key = `${c.x + 1}:${c.y + 1}`;
if (c.coordinateKey != null && c.coordinateKey !== key) return null;
return key;
};
const resolvePixels = (messages) => {
const tombstoned = buildValidatedTombstoneSet(messages);
const byKey = new Map();
for (const m of messages) {
const c = m.value?.content;
if (!c || c.type !== 'pixelia' || !m.key) continue;
const coordinateKey = pixelCoordinate(c);
if (!coordinateKey || typeof c.color !== 'string' || !COLOR_RE.test(c.color)) continue;
byKey.set(m.key, { m, coordinateKey });
}
const replaced = new Set();
for (const { m, coordinateKey } of byKey.values()) {
const prev = byKey.get(m.value.content.replaces);
if (prev && prev.coordinateKey === coordinateKey) replaced.add(prev.m.key);
}
const latest = new Map();
for (const [k, { m, coordinateKey }] of byKey) {
if (replaced.has(k) || tombstoned.has(k)) continue;
const cur = latest.get(coordinateKey);
const ts = m.value.timestamp || 0;
const curTs = cur ? cur.value.timestamp || 0 : 0;
if (!cur || ts > curTs || (ts === curTs && k > cur.key)) latest.set(coordinateKey, m);
}
return latest;
};
module.exports = ({ cooler }) => {
let ssb;
@ -13,52 +54,22 @@ module.exports = ({ cooler }) => {
const getPixelByCoordinate = async (coordinateKey) => {
const ssbClient = await openSsb();
const messages = await new Promise((res, rej) => {
pull(
ssbClient.createLogStream({ limit: logLimit }),
pull.collect((err, msgs) => err ? rej(err) : res(msgs))
);
});
const tombstoned = buildValidatedTombstoneSet(messages);
const replaces = new Map();
const byId = new Map();
for (const m of messages) {
const c = m.value?.content;
const k = m.key;
if (!c || c.type !== 'pixelia' || c.coordinateKey !== coordinateKey) continue;
if (tombstoned.has(k)) continue;
if (c.replaces) replaces.set(c.replaces, k);
byId.set(k, m);
}
for (const [replacedId, replacingId] of replaces.entries()) {
const orig = byId.get(replacedId);
if (!orig) continue;
const rep = byId.get(replacingId);
if (!rep || rep.value.author !== orig.value.author) {
byId.delete(replacingId);
continue;
}
byId.delete(replacedId);
}
return [...byId.values()][0] || null;
const messages = await readTyped(ssbClient, PIXELIA_TYPES, { limit: logLimit });
return resolvePixels(messages).get(coordinateKey) || null;
};
const paintPixel = async (x, y, color) => {
if (x < 1 || x > 50 || y < 1 || y > 200) {
if (!Number.isInteger(x) || !Number.isInteger(y) || x < 1 || x > 50 || y < 1 || y > 200) {
throw new Error('Coordinates out of bounds. Please use x (1-50) and y (1-200)');
}
if (typeof color !== 'string' || !COLOR_RE.test(color)) throw new Error('Invalid color');
const ssbClient = await openSsb();
const userId = ssbClient.id;
const coordinateKey = `${x}:${y}`;
const existingPixel = await getPixelByCoordinate(coordinateKey);
if (existingPixel) {
if (existingPixel && existingPixel.value.author === userId) {
const tombstone = {
type: 'tombstone',
target: existingPixel.key,
@ -69,7 +80,7 @@ module.exports = ({ cooler }) => {
);
}
const contributors = existingPixel?.value?.content?.contributors_inhabitants || [];
const contributors = Array.isArray(existingPixel?.value?.content?.contributors_inhabitants) ? existingPixel.value.content.contributors_inhabitants : [];
const contributors_inhabitants = contributors.includes(userId)
? contributors
: [...contributors, userId];
@ -93,46 +104,14 @@ module.exports = ({ cooler }) => {
const listPixels = async () => {
const ssbClient = await openSsb();
const messages = await new Promise((res, rej) => {
pull(
ssbClient.createLogStream({ limit: logLimit }),
pull.collect((err, msgs) => err ? rej(err) : res(msgs))
);
});
const messages = await readTyped(ssbClient, PIXELIA_TYPES, { limit: logLimit });
const tombstoned = buildValidatedTombstoneSet(messages);
const replaces = new Map();
const byKey = new Map();
for (const m of messages) {
const c = m.value?.content;
const k = m.key;
if (!c) continue;
if (c.type === 'tombstone') continue;
if (c.type === 'pixelia') {
if (tombstoned.has(k)) continue;
if (c.replaces) replaces.set(c.replaces, k);
byKey.set(k, m);
}
}
for (const [replacedId, replacingId] of replaces.entries()) {
const orig = byKey.get(replacedId);
if (!orig) continue;
const rep = byKey.get(replacingId);
if (!rep || rep.value.author !== orig.value.author) {
byKey.delete(replacingId);
continue;
}
byKey.delete(replacedId);
}
return Array.from(byKey.values()).map(m => ({
return Array.from(resolvePixels(messages).values()).map(m => ({
x: m.value.content.x + 1,
y: m.value.content.y + 1,
color: m.value.content.color,
author: m.value.author,
contributors_inhabitants: m.value.content.contributors_inhabitants || [],
contributors_inhabitants: [...new Set([...(Array.isArray(m.value.content.contributors_inhabitants) ? m.value.content.contributors_inhabitants : []).filter(id => typeof id === 'string'), m.value.author])],
timestamp: m.value.timestamp
}));
};