OASIS_MOBILE/src/models/pixelia_model.js
oasis-bot 948b6460d9 upstream: integrar Oasis 1.3.3
Diferencia 1.3.1 -> 1.3.3 del codigo de la APK oficial aplicada a tres
bandas encima de la rama: karma en el perfil, salas con directo, fotos
selladas, visibilidad de menciones, telefono por stream en Android y
las traducciones nuevas. Sin dependencias nuevas.

OasisMobile.css sigue siendo el del fork; del oficial solo se traen al
final las reglas de las piezas nuevas (karma, directo a pantalla
completa, salas en vivo, graphos).
2026-10-19 00:30:25 +02:00

146 lines
4.8 KiB
JavaScript

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();
const parentOf = (k) => {
const { m, coordinateKey } = byKey.get(k);
const prev = byKey.get(m.value.content.replaces);
return prev && prev.coordinateKey === coordinateKey ? prev.m.key : null;
};
for (const k of byKey.keys()) {
const parent = parentOf(k);
if (parent) replaced.add(parent);
}
const depthOf = new Map();
const depth = (key) => {
const chain = [];
const inChain = new Set();
let k = key;
while (k && !depthOf.has(k) && !inChain.has(k)) {
chain.push(k);
inChain.add(k);
k = parentOf(k);
}
let d = k && depthOf.has(k) ? depthOf.get(k) + 1 : 0;
for (let i = chain.length - 1; i >= 0; i--) depthOf.set(chain[i], d++);
return depthOf.get(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);
if (!cur) { latest.set(coordinateKey, m); continue; }
const d = depth(k);
const curD = depth(cur.key);
const ts = m.value.timestamp || 0;
const curTs = cur.value.timestamp || 0;
if (d > curD || (d === curD && (ts > curTs || (ts === curTs && k > cur.key)))) latest.set(coordinateKey, m);
}
return latest;
};
module.exports = ({ cooler }) => {
let ssb;
const openSsb = async () => {
if (!ssb) ssb = await cooler.open();
return ssb;
};
const getPixelByCoordinate = async (coordinateKey) => {
const ssbClient = await openSsb();
const messages = await readTyped(ssbClient, PIXELIA_TYPES, { limit: logLimit });
return resolvePixels(messages).get(coordinateKey) || null;
};
const paintPixel = async (x, y, color) => {
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 && existingPixel.value.author === userId) {
const tombstone = {
type: 'tombstone',
target: existingPixel.key,
deletedAt: new Date().toISOString()
};
await new Promise((resolve, reject) =>
ssbClient.publish(tombstone, err => err ? reject(err) : resolve())
);
}
const contributors = Array.isArray(existingPixel?.value?.content?.contributors_inhabitants) ? existingPixel.value.content.contributors_inhabitants : [];
const contributors_inhabitants = contributors.includes(userId)
? contributors
: [...contributors, userId];
const content = {
type: 'pixelia',
x: x - 1,
y: y - 1,
color,
author: userId,
contributors_inhabitants,
timestamp: Date.now(),
coordinateKey,
replaces: existingPixel?.key || null
};
await new Promise((resolve, reject) => {
ssbClient.publish(content, (err) => err ? reject(err) : resolve());
});
};
const listPixels = async () => {
const ssbClient = await openSsb();
const messages = await readTyped(ssbClient, PIXELIA_TYPES, { limit: logLimit });
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: [...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
}));
};
return {
paintPixel,
listPixels
};
};