upstream: integrar 0.9.5 en lo que no diverge (57 ficheros) + dependencias nuevas

Primera tanda de la integracion 0.9.1 -> 0.9.5. Se actualizan los 57 ficheros que
upstream cambio y la rama no tocaba, entre ellos los 11 de traduccion: eso ya solo
es posible gracias al overlay de i18n, antes eran 11 conflictos garantizados.

Se portan ademas las dependencias que 0.9.5 introduce y de las que dependen los
ficheros actualizados: pdfDocument, content_favorites, recurrence, media_gallery,
comments_view, gallery_view y polls (modelo, limites y vista).

Quedan 48 ficheros con divergencia real por resolver a mano (backend.js,
main_views.js, OasisMobile.css y 45 vistas y modelos).

Verificado: 0 errores de sintaxis en src/, 0 requires relativos rotos, la app
arranca declarando 0.9.5 y las 47 rutas principales responden 200/302.
This commit is contained in:
SITO 2026-08-18 21:42:59 +02:00
parent b3986b12a0
commit f964e61a1c
67 changed files with 5740 additions and 10907 deletions

View file

@ -14,25 +14,26 @@ const normalizeTags = (raw) => {
return String(raw).split(",").map(t => t.trim()).filter(Boolean)
}
const hasAnyInterval = (w, m, y) => !!(w || m || y)
const expandRecurrence = (firstDate, deadline, weekly, monthly, yearly) => {
const start = new Date(firstDate)
const out = [start]
if (!deadline || !hasAnyInterval(weekly, monthly, yearly)) return out
const end = new Date(deadline).getTime()
const seen = new Set([start.getTime()])
const walk = (mutate) => {
const n = new Date(start)
mutate(n)
while (n.getTime() <= end) {
const t = n.getTime()
if (!seen.has(t)) { seen.add(t); out.push(new Date(n)) }
mutate(n)
}
}
if (weekly) walk((d) => d.setDate(d.getDate() + 7))
if (monthly) walk((d) => d.setMonth(d.getMonth() + 1))
if (yearly) walk((d) => d.setFullYear(d.getFullYear() + 1))
return out.sort((a, b) => a.getTime() - b.getTime())
const { expandRecurrence } = require('./recurrence')
const ts = (v) => {
const t = new Date(v).getTime()
return Number.isFinite(t) ? t : null
}
const assertCalendarDates = ({ deadline, date, until }) => {
const now = Date.now()
const dl = deadline ? ts(deadline) : null
const dt = date ? ts(date) : null
const un = until ? ts(until) : null
if (deadline && dl === null) throw new Error("Deadline is not a valid date")
if (date && dt === null) throw new Error("Date is not a valid date")
if (until && un === null) throw new Error("The recurrence end is not a valid date")
if (dl !== null && dl <= now) throw new Error("Deadline must be in the future")
if (dt !== null && dt <= now) throw new Error("Date must be in the future")
if (dt !== null && dl !== null && dt > dl) throw new Error("Date cannot be later than the deadline")
if (un !== null && dt !== null && un < dt) throw new Error("The recurrence cannot end before it starts")
if (un !== null && dl !== null && un > dl) throw new Error("The recurrence cannot end after the deadline")
}
module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel }) => {
@ -108,6 +109,12 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
return tribeCrypto.encryptContent(content, [key], true)
}
const decryptScoped = async (content, rootId) => {
if (!content) return content
if (content.tribeId) return await decryptIfTribe(content)
return decryptCalendarRoot(content, rootId)
}
const decryptCalendarRoot = (content, rootId) => {
if (!content || !content.encryptedPayload) return content
if (!tribeCrypto) return content
@ -259,6 +266,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
status: c.status || "OPEN",
deadline: undec ? "" : (c.deadline || ""),
tags: Array.isArray(c.tags) ? c.tags : [],
mapUrl: typeof c.mapUrl === "string" ? c.mapUrl : "",
author: c.author || node.author,
participants: Array.isArray(participants) ? participants : (Array.isArray(c.participants) ? c.participants : []),
invites: Array.isArray(c.invites) ? c.invites : [],
@ -339,14 +347,18 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
return tip
},
async createCalendar({ title, status, deadline, tags, firstDate, firstDateLabel, firstNote, intervalWeekly, intervalMonthly, intervalYearly, tribeId }) {
async createCalendar({ title, status, deadline, tags, firstDate, firstDateLabel, firstNote, intervalWeekly, intervalMonthly, intervalYearly, intervalDeadline, mapUrl, tribeId }) {
const ssbClient = await openSsb()
const userId = ssbClient.id
const now = new Date().toISOString()
const validStatus = ["OPEN", "CLOSED"].includes(String(status).toUpperCase()) ? String(status).toUpperCase() : "OPEN"
if (deadline && new Date(deadline).getTime() <= Date.now()) throw new Error("Deadline must be in the future")
if (!firstDate || new Date(firstDate).getTime() <= Date.now()) throw new Error("First date must be in the future")
if (!firstDate) throw new Error("First date must be in the future")
assertCalendarDates({
deadline,
date: firstDate,
until: hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) ? intervalDeadline : ""
})
let plainContent = {
type: "calendar",
@ -354,6 +366,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
status: validStatus,
deadline: deadline || "",
tags: normalizeTags(tags),
mapUrl: safeText(mapUrl),
author: userId,
participants: [userId],
invites: [],
@ -400,6 +413,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
status: validStatus,
deadline: dec.deadline || "",
tags: Array.isArray(dec.tags) ? dec.tags : [],
mapUrl: dec.mapUrl || "",
author: userId,
participants: [userId],
invites: [{ code: pubCode, ek, salt: inviteSalt, gen: 1, public: true }],
@ -424,7 +438,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
...(intervalWeekly ? { intervalWeekly: true } : {}),
...(intervalMonthly ? { intervalMonthly: true } : {}),
...(intervalYearly ? { intervalYearly: true } : {}),
...(deadline && hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) ? { intervalDeadline: deadline } : {}),
...(hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly) && (intervalDeadline || deadline) ? { intervalDeadline: intervalDeadline || deadline } : {}),
...(tribeId ? { tribeId } : {})
}
if (tribeId) dateContent = await encryptIfTribe(dateContent)
@ -467,12 +481,15 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
: decryptCalendarRoot(item.content, rootId)
assertReadable(oldDec, "Calendar")
if ((oldDec.author || item.content.author) !== userId) throw new Error("Not the author")
const nextDeadline = data.deadline !== undefined ? data.deadline : (oldDec.deadline || "")
if (nextDeadline !== (oldDec.deadline || "")) assertCalendarDates({ deadline: nextDeadline })
let updated = {
type: "calendar",
title: data.title !== undefined ? safeText(data.title) : (oldDec.title || ""),
status: data.status !== undefined ? (["OPEN","CLOSED"].includes(String(data.status).toUpperCase()) ? String(data.status).toUpperCase() : oldDec.status) : (oldDec.status || "OPEN"),
deadline: data.deadline !== undefined ? data.deadline : (oldDec.deadline || ""),
deadline: nextDeadline,
tags: data.tags !== undefined ? normalizeTags(data.tags) : (Array.isArray(oldDec.tags) ? oldDec.tags : []),
mapUrl: data.mapUrl !== undefined ? safeText(data.mapUrl) : (oldDec.mapUrl || ""),
author: oldDec.author || userId,
participants: oldDec.participants || [userId],
invites: Array.isArray(oldDec.invites) ? oldDec.invites : [],
@ -495,7 +512,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
const userId = ssbClient.id
const item = await new Promise((resolve, reject) => ssbClient.get(tipId, (e, it) => e ? reject(e) : resolve(it)))
if (!item || !item.content) throw new Error("Calendar not found")
const dec = await decryptIfTribe(item.content)
const rootId = await this.resolveRootId(id)
const dec = await decryptScoped(item.content, rootId)
assertReadable(dec, "Calendar")
const contentAuthor = (dec && dec.author) || (typeof item.content === 'object' && item.content.author)
if (contentAuthor !== userId) throw new Error("Not the author")
@ -588,9 +606,14 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
const cal = await this.getCalendarById(rootId)
if (!cal) throw new Error("Calendar not found")
if (cal.status === "CLOSED" && userId !== cal.author) throw new Error("Only the author can add dates to a CLOSED calendar")
if (!date || new Date(date).getTime() <= Date.now()) throw new Error("Date must be in the future")
if (!date) throw new Error("Date must be in the future")
const hasInterval = hasAnyInterval(intervalWeekly, intervalMonthly, intervalYearly)
assertCalendarDates({
deadline: cal.deadline || "",
date,
until: hasInterval ? intervalDeadline : ""
})
const ruleDeadline = hasInterval ? (intervalDeadline || cal.deadline || "") : ""
let dateContent = {
type: "calendarDate",
@ -748,8 +771,9 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
const userId = ssbClient.id
const item = await new Promise((resolve, reject) => ssbClient.get(noteId, (e, it) => e ? reject(e) : resolve(it)))
if (!item || !item.content) throw new Error("Note not found")
const dec = await decryptIfTribe(item.content)
if ((dec.author || item.content.author) !== userId) throw new Error("Not the author")
const noteRoot = item.content.calendarId || null
const dec = await decryptScoped(item.content, noteRoot)
if (((dec && dec.author) || item.content.author) !== userId) throw new Error("Not the author")
return new Promise((resolve, reject) => {
ssbClient.publish({ type: "tombstone", target: noteId, deletedAt: new Date().toISOString(), author: userId }, (e, msg) => e ? reject(e) : resolve(msg))
})
@ -777,8 +801,8 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
if (tombstoned.has(m.key)) continue
if (c.calendarId !== rootId || c.dateId !== dateId) continue
let dec = c
if (c.encryptedPayload && tribeCrypto && tribesModel) {
const r = await tribeCrypto.decryptFromTribe(c, tribesModel)
if (c.encryptedPayload) {
const r = await decryptScoped(c, rootId)
if (r && !r._undecryptable) dec = r
else continue
}
@ -929,6 +953,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
status: dec.status || "OPEN",
deadline: dec.deadline || "",
tags: Array.isArray(dec.tags) ? dec.tags : [],
mapUrl: dec.mapUrl || "",
author: dec.author,
participants: Array.isArray(dec.participants) ? dec.participants : [userId],
invites,
@ -975,6 +1000,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
status: dec.status || "OPEN",
deadline: dec.deadline || "",
tags: Array.isArray(dec.tags) ? dec.tags : [],
mapUrl: dec.mapUrl || "",
author: dec.author,
participants: Array.isArray(dec.participants) ? dec.participants : [userId],
invites,
@ -1009,7 +1035,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
let calKey = null
if (tribeCrypto && typeof matchedInvite === "object") {
if (matchedInvite.ekChain) {
const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt)
const chain = tribeCrypto.decryptChainFromInvite(matchedInvite.ekChain, code, matchedInvite.salt, 3)
if (Array.isArray(chain) && chain.length) {
for (const entry of chain) {
if (Array.isArray(entry.keys) && entry.keys.length) {
@ -1043,6 +1069,7 @@ module.exports = ({ cooler, pmModel, tribeCrypto, calendarCrypto, tribesModel })
status: dec.status || "OPEN",
deadline: dec.deadline || "",
tags: Array.isArray(dec.tags) ? dec.tags : [],
mapUrl: dec.mapUrl || "",
author: dec.author,
participants: [...(Array.isArray(dec.participants) ? dec.participants : []), userId],
invites,