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).
146 lines
4.8 KiB
JavaScript
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
|
|
};
|
|
};
|