integrar la 1.3.5 del dev y perfiles Minimo, Acampada y Activista

Lo del dev, tal cual desde el nodejs-project de su APK oficial 1.3.5, mezclado
a tres bandas con la 1.3.3 como base. Lo que no habiamos tocado se coge entero.

Conflictos:
- backend.js: su middleware nuevo de media va delante de nuestro router.
- main_views.js y bookmark_view.js: lo suyo. Su menu de tarjeta (⋯) ya aparta
  borrar del pulgar, que era lo que hacia nuestro cambio.
- settings_view.js y tribes_view.js: lo suyo mas nuestra linea de enganche.

La 1.3.5 trae una cabecera de modulo para el movil (apkModbar) que se lleva la
primera caja de fichas, el buscador y los contadores. Con el skin Activista su
CSS (OasisMobile.css) no se carga: se le da estilo en mobile_ux.css, los avisos
suben encima con order y se ajusta la cabecera nueva de las tarjetas. Filtrar
va ahora dentro de la caja de fichas (casilla + etiqueta) para viajar con ella.

Perfiles: Minimo (6 modulos, de serie), Acampada (16), Activista (24, con
tribus, votaciones, campañas, encuestas y tareas) y despues los del dev por
numero de modulos.
This commit is contained in:
hacklab 2026-10-22 12:12:20 +02:00
parent ff91ef938c
commit 83f26ba19e
157 changed files with 6633 additions and 4710 deletions

View file

@ -230,7 +230,141 @@ const stripPdfMetadata = (buffer) => {
}
};
const STRIP_CHUNK = 64 * 1024;
const STRIP_IN_MEMORY_MAX = 256 * 1024 * 1024;
const FFMPEG_STRIP_MS = 120000;
const MP4_LIKE = /mp4|quicktime|m4a|m4v|3gpp/;
const MP4_METADATA_BOXES = new Set(['udta', 'meta']);
const MP4_WALKED_BOXES = new Set(['moov', 'trak']);
const XMP_UUID = Buffer.from('be7acfcb97a942e89c71999491e3afac', 'hex');
const WAV_METADATA_CHUNKS = new Set(['LIST', 'id3 ', 'ID3 ', 'bext', 'iXML', '_PMX', 'XMP ']);
const FLAC_METADATA_BLOCKS = new Set([4, 6]);
const FLAC_PADDING = 1;
const ID3V1_NO_GENRE = 255;
const FFMPEG_FORMATS = { 'video/webm': 'webm', 'audio/webm': 'webm', 'video/x-matroska': 'matroska', 'audio/x-matroska': 'matroska', 'video/ogg': 'ogg', 'audio/ogg': 'ogg', 'audio/opus': 'ogg', 'video/x-msvideo': 'avi' };
const readAt = async (fh, pos, len) => {
const buf = Buffer.alloc(len);
const { bytesRead } = await fh.read(buf, 0, len, pos);
return buf.subarray(0, bytesRead);
};
const zeroAt = async (fh, pos, len) => {
const zeros = Buffer.alloc(Math.min(len, STRIP_CHUNK));
for (let done = 0; done < len; done += zeros.length) await fh.write(zeros, 0, Math.min(zeros.length, len - done), pos + done);
};
const stripMp4 = async (fh, start, end, depth = 0) => {
let pos = start;
while (pos + 8 <= end) {
const head = await readAt(fh, pos, 16);
if (head.length < 8) return;
let size = head.readUInt32BE(0);
let header = 8;
const type = head.toString('latin1', 4, 8);
if (size === 1) { if (head.length < 16) return; size = Number(head.readBigUInt64BE(8)); header = 16; }
else if (size === 0) size = end - pos;
if (size < header || pos + size > end) return;
const isXmp = type === 'uuid' && (await readAt(fh, pos + header, XMP_UUID.length)).equals(XMP_UUID);
if (MP4_METADATA_BOXES.has(type) || isXmp) {
await fh.write(Buffer.from('free', 'latin1'), 0, 4, pos + 4);
await zeroAt(fh, pos + header, size - header);
}
else if (MP4_WALKED_BOXES.has(type) && depth < MP4_WALKED_BOXES.size) await stripMp4(fh, pos + header, pos + size, depth + 1);
pos += size;
}
};
const stripMp3 = async (fh, size) => {
const head = await readAt(fh, 0, 10);
if (head.length === 10 && head.toString('latin1', 0, 3) === 'ID3') {
const tagSize = ((head[6] & 0x7f) << 21) | ((head[7] & 0x7f) << 14) | ((head[8] & 0x7f) << 7) | (head[9] & 0x7f);
const footer = head[5] & 0x10 ? 10 : 0;
await fh.write(Buffer.from([0]), 0, 1, 5);
await zeroAt(fh, 10, Math.min(tagSize + footer, size - 10));
}
if (size >= 128 && (await readAt(fh, size - 128, 3)).toString('latin1') === 'TAG') {
await zeroAt(fh, size - 125, 125);
await fh.write(Buffer.from([ID3V1_NO_GENRE]), 0, 1, size - 1);
}
};
const stripFlac = async (fh, size) => {
if ((await readAt(fh, 0, 4)).toString('latin1') !== 'fLaC') return;
for (let pos = 4; pos + 4 <= size;) {
const head = await readAt(fh, pos, 4);
if (head.length < 4) return;
const last = head[0] & 0x80;
const len = head.readUIntBE(1, 3);
if (FLAC_METADATA_BLOCKS.has(head[0] & 0x7f)) {
await fh.write(Buffer.from([last | FLAC_PADDING]), 0, 1, pos);
await zeroAt(fh, pos + 4, len);
}
if (last) return;
pos += 4 + len;
}
};
const stripWav = async (fh, size) => {
const head = await readAt(fh, 0, 12);
if (head.length < 12 || head.toString('latin1', 0, 4) !== 'RIFF' || head.toString('latin1', 8, 12) !== 'WAVE') return;
for (let pos = 12; pos + 8 <= size;) {
const chunk = await readAt(fh, pos, 8);
if (chunk.length < 8) return;
const len = chunk.readUInt32LE(4);
if (WAV_METADATA_CHUNKS.has(chunk.toString('latin1', 0, 4))) {
await fh.write(Buffer.from('JUNK', 'latin1'), 0, 4, pos);
await zeroAt(fh, pos + 8, Math.min(len, size - pos - 8));
}
pos += 8 + len + (len & 1);
}
};
const stripWithFfmpeg = (filepath, format) => new Promise((resolve) => {
const out = `${filepath}.clean`;
const args = ['-v', 'error', '-y', '-i', filepath, '-map', '0', '-map_metadata', '-1', '-c', 'copy', '-f', format, out];
require('child_process').execFile(process.env.OASIS_FFMPEG || 'ffmpeg', args, { timeout: FFMPEG_STRIP_MS }, async (err) => {
try {
if (err) await promisesFs.unlink(out).catch(() => {});
else await promisesFs.rename(out, filepath);
} catch (_) {}
resolve();
});
});
const stripFileMetadata = async (filepath, mime) => {
const m = String(mime || '');
try {
const { size } = await promisesFs.stat(filepath);
if ((m.startsWith('image/') && m !== 'image/gif') || m === 'application/pdf') {
if (size > STRIP_IN_MEMORY_MAX) return;
const data = await promisesFs.readFile(filepath);
const clean = m === 'application/pdf' ? stripPdfMetadata(data) : await stripImageMetadata(data);
if (clean !== data) await promisesFs.writeFile(filepath, clean);
return;
}
if (!m.startsWith('audio/') && !m.startsWith('video/')) return;
if (FFMPEG_FORMATS[m]) { await stripWithFfmpeg(filepath, FFMPEG_FORMATS[m]); return; }
const fh = await promisesFs.open(filepath, 'r+');
try {
if (MP4_LIKE.test(m)) await stripMp4(fh, 0, size);
else if (m === 'audio/mpeg') await stripMp3(fh, size);
else if (m === 'audio/flac' || m === 'audio/x-flac') await stripFlac(fh, size);
else if (/wav|wave/.test(m)) await stripWav(fh, size);
} finally { await fh.close(); }
} catch (_) {}
};
const MAX_BLOB_SIZE = 75 * 1024 * 1024;
const KIND_MIME = { image: /^image\//, audio: /^audio\//, video: /^video\//, document: /^application\/pdf$/, torrent: /^application\/x-bittorrent$/ };
const looksLikeTorrent = (data) => data.length > 0 && data[0] === 0x64 && data.subarray(0, 4096).includes(Buffer.from('4:info'));
class WrongFileTypeError extends Error {
constructor(fileName, mime) {
super(`Wrong file type: ${fileName} (${mime})`);
this.name = 'WrongFileTypeError';
}
}
class FileTooLargeError extends Error {
constructor(fileName, fileSize) {
@ -253,7 +387,7 @@ const oggMime = (buffer) => {
const sealedRef = (sealed, mime, name) => `/blob/sealed/${encodeURIComponent(sealed.manifestBlobId)}?k=${encodeURIComponent(sealed.key)}&t=${encodeURIComponent(mime)}&n=${encodeURIComponent(name)}`;
const storeUploadedFile = async function (blobUpload, seal = null) {
const storeUploadedFile = async function (blobUpload, seal = null, kind = null) {
if (!blobUpload || !blobUpload.filepath) return null;
let data = await promisesFs.readFile(blobUpload.filepath);
@ -294,11 +428,16 @@ const storeUploadedFile = async function (blobUpload, seal = null) {
if (!blob.mime) {
blob.mime = 'application/octet-stream';
}
if (blob.mime === 'application/octet-stream' && looksLikeTorrent(data)) blob.mime = 'application/x-bittorrent';
if (kind && KIND_MIME[kind] && !KIND_MIME[kind].test(blob.mime)) throw new WrongFileTypeError(blob.name, blob.mime);
if (blob.mime.startsWith('image/') && blob.mime !== 'image/gif') {
data = await stripImageMetadata(data);
} else if (blob.mime === 'application/pdf') {
data = stripPdfMetadata(data);
} else if (blob.mime.startsWith('audio/') || blob.mime.startsWith('video/')) {
await stripFileMetadata(blobUpload.filepath, blob.mime);
data = await promisesFs.readFile(blobUpload.filepath);
}
if (typeof seal === 'function') {
@ -324,11 +463,11 @@ const storeUploadedFile = async function (blobUpload, seal = null) {
return `\n[${blob.name}](${blob.id})`;
};
const handleBlobUpload = async function (ctx, fileFieldName, seal = null) {
const handleBlobUpload = async function (ctx, fileFieldName, seal = null, kind = null) {
if (!ctx.request.files || !ctx.request.files[fileFieldName]) return null;
const entry = ctx.request.files[fileFieldName];
const first = Array.isArray(entry) ? entry[0] : entry;
return storeUploadedFile(first, seal);
return storeUploadedFile(first, seal, kind);
};
const handleBlobUploads = async function (ctx, fileFieldName, max = 8, seal = null) {
@ -507,5 +646,5 @@ const serveBlob = async function (ctx) {
sendBlobBuffer(ctx, buffer);
};
module.exports = { handleBlobUpload, handleBlobUploads, serveBlob, sendBlobBuffer, contentDisposition, oggMime, FileTooLargeError, stripImageMetadata };
module.exports = { handleBlobUpload, handleBlobUploads, serveBlob, sendBlobBuffer, contentDisposition, oggMime, FileTooLargeError, WrongFileTypeError, stripImageMetadata, stripFileMetadata };