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:
parent
8d838dd8b1
commit
c332afd6ea
265 changed files with 80970 additions and 29167 deletions
|
|
@ -1,4 +1,5 @@
|
|||
const pull = require('../server/node_modules/pull-stream');
|
||||
const { readTyped, requestScope } = require('./typed_log');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
|
|
@ -10,6 +11,7 @@ const HOUSE_KEYS = ['academia','solaris','arrakis','terraverde','unsystem','dogm
|
|||
const VALID_KEY = (k) => HOUSE_KEYS.includes(String(k || '').toLowerCase());
|
||||
|
||||
const TEST_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000;
|
||||
const ACADEMIA_GRACE_MS = 10 * 60 * 1000;
|
||||
|
||||
const PROFILE_QUESTIONS = [
|
||||
{
|
||||
|
|
@ -173,6 +175,12 @@ function getGoverningPeriodId(now = new Date()) {
|
|||
module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
||||
let ssb;
|
||||
const openSsb = async () => { if (!ssb) ssb = await cooler.open(); return ssb; };
|
||||
const startedAt = Date.now();
|
||||
let academiaRetry = null;
|
||||
const followsSomeone = async (client, me) => {
|
||||
const contacts = await readTyped(client, ['contact']).catch(() => []);
|
||||
return contacts.some((m) => m && m.value && m.value.author === me && m.value.content && m.value.content.following === true);
|
||||
};
|
||||
|
||||
const houseTribeTag = (houseKey) => {
|
||||
const h = HOUSES[houseKey];
|
||||
|
|
@ -192,7 +200,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
return tags.includes(tag) && members.includes(me);
|
||||
});
|
||||
if (!candidates.length) return null;
|
||||
candidates.sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime());
|
||||
candidates.sort((a, b) => (new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()) || String(a.id || '').localeCompare(String(b.id || '')));
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
|
|
@ -206,78 +214,59 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
return tags.includes(tag);
|
||||
});
|
||||
if (!candidates.length) return null;
|
||||
candidates.sort((a, b) => new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime());
|
||||
candidates.sort((a, b) => (new Date(a.createdAt || 0).getTime() - new Date(b.createdAt || 0).getTime()) || String(a.id || '').localeCompare(String(b.id || '')));
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
const ANCHOR_TYPES = ['larpHouseTribeAnchor', 'larpHouseTribeAnchorTombstone', 'tombstone'];
|
||||
const authorOfMsg = (client, key) => new Promise((resolve) => client.get(key, (err, v) => resolve(err || !v ? null : v.author || null)));
|
||||
|
||||
async function findEarliestHouseAnchor(houseKey) {
|
||||
if (!VALID_KEY(houseKey)) return null;
|
||||
const client = await openSsb();
|
||||
return new Promise((resolve) => {
|
||||
const anchors = [];
|
||||
const anchorTombstones = [];
|
||||
const tribeTombstones = new Map();
|
||||
const msgAuthorByKey = new Map();
|
||||
pull(
|
||||
client.createLogStream(),
|
||||
pull.drain((m) => {
|
||||
if (!m || !m.value) return;
|
||||
const author = m.value.author;
|
||||
if (m.key && author) msgAuthorByKey.set(m.key, author);
|
||||
const c = m.value.content;
|
||||
if (!c || typeof c !== 'object') return;
|
||||
if (c.type === 'larpHouseTribeAnchor') {
|
||||
if (c.house !== houseKey) return;
|
||||
if (typeof c.tribeRootId !== 'string') return;
|
||||
const tribeTs = Number(Date.parse(c.tribeCreatedAt || '')) || m.value.timestamp || 0;
|
||||
anchors.push({ tribeRootId: c.tribeRootId, anchorAuthor: author, tribeTs });
|
||||
} else if (c.type === 'larpHouseTribeAnchorTombstone') {
|
||||
if (c.house !== houseKey) return;
|
||||
if (typeof c.tribeRootId !== 'string') return;
|
||||
anchorTombstones.push({ tribeRootId: c.tribeRootId, tombstoneAuthor: author });
|
||||
} else if (c.type === 'tombstone' && typeof c.target === 'string') {
|
||||
tribeTombstones.set(c.target, author);
|
||||
}
|
||||
}, () => {
|
||||
const killedAnchorPairs = new Set();
|
||||
for (const t of anchorTombstones) {
|
||||
killedAnchorPairs.add(`${t.tribeRootId}|${t.tombstoneAuthor}`);
|
||||
}
|
||||
const deadTribes = new Set();
|
||||
for (const [target, tombAuthor] of tribeTombstones) {
|
||||
const tribeAuthor = msgAuthorByKey.get(target);
|
||||
if (tribeAuthor && tribeAuthor === tombAuthor) deadTribes.add(target);
|
||||
}
|
||||
const live = anchors.filter(a =>
|
||||
!killedAnchorPairs.has(`${a.tribeRootId}|${a.anchorAuthor}`) &&
|
||||
!deadTribes.has(a.tribeRootId)
|
||||
);
|
||||
if (!live.length) return resolve(null);
|
||||
live.sort((a, b) => a.tribeTs - b.tribeTs);
|
||||
const first = live[0];
|
||||
resolve({ tribeRootId: first.tribeRootId, author: first.anchorAuthor, tribeTs: first.tribeTs });
|
||||
})
|
||||
);
|
||||
});
|
||||
const msgs = await readTyped(client, ANCHOR_TYPES);
|
||||
const anchors = [];
|
||||
const killedAnchorPairs = new Set();
|
||||
const tombAuthors = new Map();
|
||||
for (const m of msgs) {
|
||||
const c = m && m.value && m.value.content;
|
||||
if (!c || typeof c !== 'object') continue;
|
||||
const author = m.value.author;
|
||||
if (c.type === 'larpHouseTribeAnchor') {
|
||||
if (c.house !== houseKey || typeof c.tribeRootId !== 'string') continue;
|
||||
const tribeTs = Number(Date.parse(c.tribeCreatedAt || '')) || m.value.timestamp || 0;
|
||||
anchors.push({ tribeRootId: c.tribeRootId, anchorAuthor: author, tribeTs });
|
||||
} else if (c.type === 'larpHouseTribeAnchorTombstone') {
|
||||
if (c.house !== houseKey || typeof c.tribeRootId !== 'string') continue;
|
||||
killedAnchorPairs.add(`${c.tribeRootId}|${author}`);
|
||||
} else if (c.type === 'tombstone' && typeof c.target === 'string') {
|
||||
if (!tombAuthors.has(c.target)) tombAuthors.set(c.target, new Set());
|
||||
tombAuthors.get(c.target).add(author);
|
||||
}
|
||||
}
|
||||
const candidates = anchors.filter(a => !killedAnchorPairs.has(`${a.tribeRootId}|${a.anchorAuthor}`));
|
||||
const deadTribes = new Set();
|
||||
await Promise.all([...new Set(candidates.map(a => a.tribeRootId).filter(t => tombAuthors.has(t)))].map(async (t) => {
|
||||
const tribeAuthor = await authorOfMsg(client, t);
|
||||
if (tribeAuthor && tombAuthors.get(t).has(tribeAuthor)) deadTribes.add(t);
|
||||
}));
|
||||
const live = candidates.filter(a => !deadTribes.has(a.tribeRootId));
|
||||
if (!live.length) return null;
|
||||
live.sort((a, b) => a.tribeTs - b.tribeTs);
|
||||
const first = live[0];
|
||||
return { tribeRootId: first.tribeRootId, author: first.anchorAuthor, tribeTs: first.tribeTs };
|
||||
}
|
||||
|
||||
async function findHouseAnchorByTribe(houseKey, tribeRootId) {
|
||||
if (!VALID_KEY(houseKey) || !tribeRootId) return null;
|
||||
const client = await openSsb();
|
||||
return new Promise((resolve) => {
|
||||
let hit = null;
|
||||
pull(
|
||||
client.createLogStream(),
|
||||
pull.drain((m) => {
|
||||
if (hit) return;
|
||||
const c = m && m.value && m.value.content;
|
||||
if (!c || c.type !== 'larpHouseTribeAnchor') return;
|
||||
if (c.house !== houseKey) return;
|
||||
if (c.tribeRootId !== tribeRootId) return;
|
||||
hit = { author: m.value.author, ts: m.value.timestamp || 0 };
|
||||
}, () => resolve(hit))
|
||||
);
|
||||
});
|
||||
const msgs = await readTyped(client, ANCHOR_TYPES);
|
||||
for (const m of msgs) {
|
||||
const c = m && m.value && m.value.content;
|
||||
if (!c || c.type !== 'larpHouseTribeAnchor' || c.house !== houseKey || c.tribeRootId !== tribeRootId) continue;
|
||||
return { author: m.value.author, ts: m.value.timestamp || 0 };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function publishHouseTribeAnchor(houseKey, tribeRootId, tribeCreatedAt) {
|
||||
|
|
@ -332,7 +321,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
const canonical = await findAnyHouseTribe(houseKey).catch(() => null);
|
||||
const canonRoot = canonical ? await tribesModel.getRootId(canonical.id).catch(() => canonical.id) : null;
|
||||
const myTribes = await listMyHouseTribes(houseKey);
|
||||
myTribes.sort((a, b) => a.createdAtTs - b.createdAtTs);
|
||||
myTribes.sort((a, b) => (a.createdAtTs - b.createdAtTs) || String(a.rootId || '').localeCompare(String(b.rootId || '')));
|
||||
const keepRoot = canonRoot || (myTribes[0] && myTribes[0].rootId);
|
||||
for (const t of myTribes) {
|
||||
if (t.rootId === keepRoot) continue;
|
||||
|
|
@ -345,6 +334,13 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
await redeemPendingAutoInvites().catch(() => {});
|
||||
return await findMyHouseTribe(houseKey) || canonical;
|
||||
}
|
||||
if (Date.now() - startedAt < ACADEMIA_GRACE_MS && await followsSomeone(client, me)) {
|
||||
if (!academiaRetry) {
|
||||
academiaRetry = setTimeout(() => { academiaRetry = null; enqueue(runCatchup); }, ACADEMIA_GRACE_MS - (Date.now() - startedAt));
|
||||
if (academiaRetry.unref) academiaRetry.unref();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
const house = HOUSES[houseKey] || {};
|
||||
const tag = houseTribeTag(houseKey);
|
||||
try {
|
||||
|
|
@ -460,6 +456,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
joinedAt: new Date().toISOString()
|
||||
}, (err, msg) => err ? reject(err) : resolve(msg));
|
||||
});
|
||||
forgetMemberships();
|
||||
if (previousHouse && previousHouse !== houseKey) {
|
||||
await leaveMyHouseTribe(previousHouse).catch(() => {});
|
||||
}
|
||||
|
|
@ -471,53 +468,45 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
async function getUserHouse(feedId) {
|
||||
const client = await openSsb();
|
||||
const target = feedId || client.id;
|
||||
return new Promise((resolve) => {
|
||||
let latest = null;
|
||||
let latestTs = 0;
|
||||
pull(
|
||||
client.createUserStream({ id: target, reverse: true }),
|
||||
pull.drain((m) => {
|
||||
const c = m && m.value && m.value.content;
|
||||
if (!c) return;
|
||||
const ts = m.value.timestamp || 0;
|
||||
if (c.type === 'larpJoinHouse' && VALID_KEY(c.house)) {
|
||||
if (ts > latestTs) { latestTs = ts; latest = c.house; }
|
||||
} else if (c.type === 'larpLeaveLarp') {
|
||||
if (ts > latestTs) { latestTs = ts; latest = null; }
|
||||
}
|
||||
}, () => resolve(latest))
|
||||
);
|
||||
});
|
||||
return (await listAllMemberships()).get(target) || null;
|
||||
}
|
||||
|
||||
async function listAllMemberships() {
|
||||
async function readMemberships() {
|
||||
const client = await openSsb();
|
||||
return new Promise((resolve) => {
|
||||
const byAuthor = new Map();
|
||||
pull(
|
||||
client.createLogStream({ reverse: true }),
|
||||
pull.drain((m) => {
|
||||
const author = m && m.value && m.value.author;
|
||||
if (!author) return;
|
||||
const c = m.value.content;
|
||||
if (!c) return;
|
||||
const ts = m.value.timestamp || 0;
|
||||
if (c.type === 'larpJoinHouse' && VALID_KEY(c.house)) {
|
||||
const prev = byAuthor.get(author);
|
||||
if (!prev || ts > prev.ts) byAuthor.set(author, { house: c.house, ts });
|
||||
} else if (c.type === 'larpLeaveLarp') {
|
||||
const prev = byAuthor.get(author);
|
||||
if (!prev || ts > prev.ts) byAuthor.set(author, { house: null, ts });
|
||||
}
|
||||
}, () => {
|
||||
const result = new Map();
|
||||
for (const [a, v] of byAuthor.entries()) {
|
||||
if (v.house) result.set(a, v.house);
|
||||
}
|
||||
resolve(result);
|
||||
})
|
||||
);
|
||||
});
|
||||
const msgs = await readTyped(client, ['larpJoinHouse', 'larpLeaveLarp'], { limit: 0 });
|
||||
const byAuthor = new Map();
|
||||
for (const m of msgs) {
|
||||
const author = m && m.value && m.value.author;
|
||||
if (!author) continue;
|
||||
const c = m.value.content;
|
||||
if (!c) continue;
|
||||
const ts = m.value.timestamp || 0;
|
||||
const seq = Number(m.value.sequence) || 0;
|
||||
const prev = byAuthor.get(author);
|
||||
const newer = !prev || ts > prev.ts || (ts === prev.ts && seq > prev.seq);
|
||||
if (c.type === 'larpJoinHouse' && VALID_KEY(c.house)) {
|
||||
if (newer) byAuthor.set(author, { house: c.house, ts, seq });
|
||||
} else if (c.type === 'larpLeaveLarp') {
|
||||
if (newer) byAuthor.set(author, { house: null, ts, seq });
|
||||
}
|
||||
}
|
||||
const result = new Map();
|
||||
for (const [a, v] of byAuthor.entries()) {
|
||||
if (v.house) result.set(a, v.house);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const forgetMemberships = () => {
|
||||
const store = requestScope.getStore();
|
||||
if (store) delete store.larpMemberships;
|
||||
};
|
||||
|
||||
async function listAllMemberships() {
|
||||
const store = requestScope.getStore();
|
||||
if (!store) return readMemberships();
|
||||
if (!store.larpMemberships) store.larpMemberships = readMemberships();
|
||||
return store.larpMemberships;
|
||||
}
|
||||
|
||||
async function publishLeaveLarp() {
|
||||
|
|
@ -530,6 +519,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
leftAt: new Date().toISOString()
|
||||
}, (err, msg) => err ? reject(err) : resolve(msg));
|
||||
});
|
||||
forgetMemberships();
|
||||
if (previousHouse) {
|
||||
await leaveMyHouseTribe(previousHouse).catch(() => {});
|
||||
}
|
||||
|
|
@ -585,10 +575,16 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
const memberships = await listAllMemberships();
|
||||
return new Promise((resolve) => {
|
||||
const posts = [];
|
||||
const tombs = new Map();
|
||||
pull(
|
||||
client.createLogStream({ reverse: true }),
|
||||
pull.drain((m) => {
|
||||
const c = m && m.value && m.value.content;
|
||||
if (c && c.type === 'tombstone' && typeof c.target === 'string') {
|
||||
if (!tombs.has(c.target)) tombs.set(c.target, new Set());
|
||||
tombs.get(c.target).add(m.value.author);
|
||||
return;
|
||||
}
|
||||
if (!c || c.type !== 'larpHousePost') return;
|
||||
if (c.house !== houseKey) return;
|
||||
const author = m.value.author;
|
||||
|
|
@ -602,8 +598,9 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
ts: m.value.timestamp || 0
|
||||
});
|
||||
}, () => {
|
||||
posts.sort((a, b) => b.ts - a.ts);
|
||||
resolve(posts);
|
||||
const shown = posts.filter(post => !(tombs.get(post.id) && tombs.get(post.id).has(post.author)));
|
||||
shown.sort((a, b) => b.ts - a.ts);
|
||||
resolve(shown);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
|
@ -770,14 +767,20 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
const ssbKeys = require('../server/node_modules/ssb-keys');
|
||||
const config = require('../server/ssb_config');
|
||||
const codes = [];
|
||||
const sealedOnly = client.private && typeof client.private.read === 'function';
|
||||
await new Promise((resolve) => {
|
||||
pull(
|
||||
client.createLogStream({ reverse: true, limit: 2000 }),
|
||||
sealedOnly ? client.private.read({ reverse: true }) : client.createLogStream({ reverse: true, limit: 2000 }),
|
||||
pull.drain((m) => {
|
||||
const c = m && m.value && m.value.content;
|
||||
if (typeof c !== 'string' || !c.endsWith('.box')) return;
|
||||
let decoded;
|
||||
try { decoded = ssbKeys.unbox(c, config.keys); } catch (_) { return; }
|
||||
if (typeof c === 'string') {
|
||||
if (!c.endsWith('.box')) return;
|
||||
try { decoded = ssbKeys.unbox(c, config.keys); } catch (_) { return; }
|
||||
} else {
|
||||
if (!m.value.private) return;
|
||||
decoded = c;
|
||||
}
|
||||
if (!decoded) return;
|
||||
if (typeof decoded === 'string') {
|
||||
try { decoded = JSON.parse(decoded); } catch (_) { return; }
|
||||
|
|
@ -832,6 +835,13 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
await issueAutoInvitesForMyHouse().catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (typeof c === 'object' && c.type === 'tribe' && Array.isArray(c.tags) && c.tags.includes(houseTribeTag('academia'))) {
|
||||
if (m.value.author === me) return;
|
||||
const myHouse = await getUserHouse(me).catch(() => null);
|
||||
if (myHouse !== 'academia') return;
|
||||
await ensureHouseTribe('academia').catch(() => {});
|
||||
return;
|
||||
}
|
||||
if (typeof c === 'object' && c.type === 'larpHouseTribeAnchor' && VALID_KEY(c.house)) {
|
||||
if (m.value.author === me) return;
|
||||
const myHouse = await getUserHouse(me).catch(() => null);
|
||||
|
|
@ -931,6 +941,7 @@ module.exports = ({ cooler, tribesModel, tribeCrypto }) => {
|
|||
publishJoin,
|
||||
publishLeaveLarp,
|
||||
getUserHouse,
|
||||
listAllMemberships,
|
||||
listHousesWithCounts,
|
||||
getMembersOfHouse,
|
||||
publishHousePost,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue