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
|
|
@ -6,7 +6,7 @@ const fs = require('fs');
|
|||
const path = require('path');
|
||||
const SecretStack = require('secret-stack');
|
||||
const caps = require('ssb-caps');
|
||||
const SSB = require('ssb-db');
|
||||
const db2Defaults = require('ssb-db2/defaults');
|
||||
const config = require('./ssb_config');
|
||||
const { printMetadata } = require('./ssb_metadata');
|
||||
|
||||
|
|
@ -27,6 +27,7 @@ const { printMetadata } = require('./ssb_metadata');
|
|||
return `${m[1].replace(/^::ffff:/, '')}:${m[2]}`;
|
||||
};
|
||||
const logRejection = (peer) => {
|
||||
if (!(process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true')) return;
|
||||
const ts = new Date().toISOString().replace('T', ' ').slice(0, 19);
|
||||
realErr.call(console, `[${ts}] REJECTED ${peer} (wrong SHS cap)`);
|
||||
};
|
||||
|
|
@ -51,35 +52,41 @@ const { printMetadata } = require('./ssb_metadata');
|
|||
};
|
||||
})();
|
||||
|
||||
require('ssb-plugins').loadUserPlugins(SecretStack({ caps }), config);
|
||||
try {
|
||||
if (require('../configs/config-manager.js').getConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1') config.conn = { ...(config.conn || {}), autostart: false };
|
||||
} catch (_) {}
|
||||
|
||||
const Server = SecretStack({ caps })
|
||||
.use(SSB)
|
||||
.use(require('ssb-db2/core'))
|
||||
.use(require('ssb-classic'))
|
||||
.use(require('ssb-box'))
|
||||
.use(require('ssb-db2/compat/publish'))
|
||||
.use(require('ssb-db2/compat/post'))
|
||||
.use(require('ssb-db2/compat/db'))
|
||||
.use(require('ssb-db2/compat/log-stream'))
|
||||
.use(require('ssb-db2/compat/history-stream'))
|
||||
.use(require('ssb-db2/compat/ebt'))
|
||||
.use(require('ssb-db2/compat/feedstate'))
|
||||
.use(require('./db2_legacy'))
|
||||
.use(require('ssb-master'))
|
||||
.use(require('ssb-gossip'))
|
||||
.use(require('ssb-ebt'))
|
||||
.use(require('ssb-friends'))
|
||||
.use(require('ssb-blobs'))
|
||||
.use(require('ssb-meme'))
|
||||
.use(require('ssb-plugins'))
|
||||
.use(require('ssb-conn'))
|
||||
.use(require('ssb-box'))
|
||||
.use(require('ssb-search'))
|
||||
.use(require('ssb-private'))
|
||||
.use(require('ssb-friend-pub'))
|
||||
.use(config.pub ? require('ssb-invite') : require('ssb-invite-client'))
|
||||
.use(require('ssb-logging'))
|
||||
.use(require('ssb-replication-scheduler'))
|
||||
.use(require('ssb-partial-replication'))
|
||||
.use(require('ssb-about'))
|
||||
.use(require('ssb-onion'))
|
||||
.use(require('ssb-unix-socket'))
|
||||
.use(require('ssb-no-auth'))
|
||||
.use(require('ssb-backlinks'))
|
||||
.use(require('ssb-links'))
|
||||
.use(require('ssb-tangle'))
|
||||
.use(require('ssb-query'));
|
||||
.use(require('./snapshot_plugin'))
|
||||
.use((() => { const pm = require('./phone_module'); try { if (process.env.OASIS_MOBILE === '1') { pm.setAudioFactory(require('./phone_audio_android')); pm.setVideoFactory(require('./phone_video_android')); } } catch (_) {} return pm; })())
|
||||
.use(require('./network_pause'));
|
||||
|
||||
if (!config.pub) {
|
||||
Server.use(require('ssb-lan'));
|
||||
Server.use(require('./lanRouter'));
|
||||
}
|
||||
|
||||
if (config.autofollow && typeof config.autofollow === 'object' && !Array.isArray(config.autofollow)) {
|
||||
if (config.autofollow.enabled === false) {
|
||||
|
|
@ -104,20 +111,135 @@ const isLockError = (err) => {
|
|||
return /Resource temporarily unavailable/i.test(msg) && /\.ssb\/.*LOCK/i.test(msg);
|
||||
};
|
||||
|
||||
const isCorruptStoreError = (err) => {
|
||||
const msg = String((err && err.message) || '');
|
||||
const stack = String((err && err.stack) || '');
|
||||
return /JSON|Unexpected token|Unexpected end|bipf|Invalid|corrupt/i.test(msg)
|
||||
&& /flumelog-offset|aligned-block-file|flumeview|flumedb|async-append-only-log|jitdb|bipf|ssb-db2/i.test(stack);
|
||||
};
|
||||
|
||||
const migration = { running: false, percent: 0, done: false, error: null };
|
||||
let migrationPromise = null;
|
||||
const MIGRATED_MARKER = 'OASIS: this log was migrated to ssb-db2';
|
||||
const oldLogIsMarker = (file) => {
|
||||
try {
|
||||
const st = fs.statSync(file);
|
||||
return st.size < 4096 && fs.readFileSync(file, 'utf8').startsWith(MIGRATED_MARKER);
|
||||
} catch (_) { return false; }
|
||||
};
|
||||
const writeMigratedMarker = () => {
|
||||
try {
|
||||
fs.mkdirSync(db2Defaults.flumePath(config.path), { recursive: true });
|
||||
fs.writeFileSync(db2Defaults.oldLogPath(config.path), `${MIGRATED_MARKER} (${config.path}/db2/log.bipf) on ${new Date().toISOString()}.\nThis file is a guard: an Oasis older than the ssb-db2 move cannot read it and refuses to start, instead of starting with an empty log and forking your feed. Do not delete it unless you are moving back to that version on purpose, and never run both versions on this folder.\n`);
|
||||
} catch (_) {}
|
||||
};
|
||||
const migrateFlume = () => {
|
||||
if (migrationPromise) return migrationPromise;
|
||||
migrationPromise = new Promise((resolve, reject) => {
|
||||
const oldLog = db2Defaults.oldLogPath(config.path);
|
||||
if (!fs.existsSync(oldLog) || oldLogIsMarker(oldLog)) { migration.done = true; return resolve(false); }
|
||||
if (isDebug()) console.log('- Database: migrating the local log to ssb-db2');
|
||||
migration.running = true;
|
||||
let mig;
|
||||
try {
|
||||
mig = SecretStack({ caps })
|
||||
.use(require('ssb-db2/migrate'))
|
||||
.call(null, { ...config, connections: { incoming: {}, outgoing: {} }, db2: { ...(config.db2 || {}), automigrate: false, dangerouslyKillFlumeWhenMigrated: true } });
|
||||
} catch (err) { migration.running = false; migration.error = err; return reject(err); }
|
||||
const pull = require('pull-stream');
|
||||
let shown = -1;
|
||||
pull(mig.db2migrate.progress(), pull.drain((p) => {
|
||||
const pct = Math.max(0, Math.min(100, Math.floor((Number(p) || 0) * 100)));
|
||||
migration.percent = pct;
|
||||
if (pct !== shown && isDebug()) { shown = pct; console.log(` migrating ${pct}%`); }
|
||||
}));
|
||||
const finish = () => {
|
||||
const waitGone = (tries) => {
|
||||
if (!fs.existsSync(db2Defaults.oldLogPath(config.path))) {
|
||||
mig.close(() => {
|
||||
writeMigratedMarker();
|
||||
migration.running = false; migration.done = true; migration.percent = 100;
|
||||
if (isDebug()) console.log(' migrated 100%');
|
||||
resolve(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (tries <= 0) {
|
||||
try { fs.rmSync(db2Defaults.flumePath(config.path), { recursive: true, force: true }); } catch (_) {}
|
||||
mig.close(() => {
|
||||
if (!fs.existsSync(db2Defaults.oldLogPath(config.path))) writeMigratedMarker();
|
||||
else if (isDebug()) console.log(' the old log could not be removed; it is left in place');
|
||||
migration.running = false; migration.done = true; migration.percent = 100;
|
||||
resolve(true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
setTimeout(() => waitGone(tries - 1), 200);
|
||||
};
|
||||
waitGone(300);
|
||||
};
|
||||
try { mig.db2migrate.start(); } catch (err) { migration.running = false; migration.error = err; return reject(err); }
|
||||
mig.db2migrate.synchronized((synced) => {
|
||||
if (synced !== true) return;
|
||||
finish();
|
||||
return false;
|
||||
});
|
||||
});
|
||||
return migrationPromise;
|
||||
};
|
||||
|
||||
const isDebug = () => process.argv.includes('--debug') || process.env.OASIS_DEBUG === '1' || process.env.OASIS_DEBUG === 'true';
|
||||
|
||||
const isPortInUseError = (err) => String((err && err.code) || '') === 'EADDRINUSE';
|
||||
|
||||
const handleFatal = (err) => {
|
||||
if (isPortInUseError(err)) {
|
||||
console.log('');
|
||||
console.log('Another Oasis instance is already running on this device. Close the other instance (or kill the process) and try again.');
|
||||
if (isDebug()) console.log(`Detail: ${String((err && err.message) || err)}`);
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
if (isLockError(err)) {
|
||||
console.log('');
|
||||
console.log('Another Oasis instance is already running on this device. Close the other instance (or kill the process) and try again.');
|
||||
if (isDebug()) {
|
||||
console.log(`Detail: ${String((err && err.message) || err)}`);
|
||||
console.log(String((err && err.stack) || '').split('\n').slice(1, 4).join('\n'));
|
||||
}
|
||||
console.log('');
|
||||
process.exit(1);
|
||||
}
|
||||
if (isCorruptStoreError(err)) {
|
||||
console.log('');
|
||||
console.log('Oasis could not read its local database (a record in ~/.ssb/db2 looks corrupted).');
|
||||
console.log('This usually happens after an unclean shutdown or a disk error.');
|
||||
console.log('');
|
||||
console.log('What you can try, in order:');
|
||||
console.log(' 1. Simply start Oasis again — transient read errors often clear on the next boot.');
|
||||
console.log(' 2. Check your disk health (e.g. run fsck on the partition holding ~/.ssb).');
|
||||
console.log(' 3. Rebuild the indexes from Settings once Oasis starts, or remove ~/.ssb/db2/indexes');
|
||||
console.log(' and ~/.ssb/db2/jit (NOT log.bipf, NOT secret) so they are regenerated from the log.');
|
||||
console.log('');
|
||||
if (isDebug()) console.log(`Technical detail: ${String((err && err.message) || err)}`);
|
||||
process.exit(1);
|
||||
}
|
||||
if (server) {
|
||||
console.error('[oasis] unexpected error, still running:', (err && err.stack) || err);
|
||||
return;
|
||||
}
|
||||
throw err;
|
||||
};
|
||||
|
||||
process.on('uncaughtException', handleFatal);
|
||||
|
||||
if (argv[0] === 'start') {
|
||||
const tightenStateDir = () => {
|
||||
try { if (config.path && fs.existsSync(config.path)) fs.chmodSync(config.path, 0o700); } catch (_) {}
|
||||
};
|
||||
|
||||
const startServerOnly = () => {
|
||||
try {
|
||||
tightenStateDir();
|
||||
server = Server(config);
|
||||
} catch (err) {
|
||||
handleFatal(err);
|
||||
|
|
@ -133,7 +255,7 @@ if (argv[0] === 'start') {
|
|||
|
||||
manifest.config = 'sync';
|
||||
server.config = cb => {
|
||||
console.log(JSON.stringify(config, null, 2));
|
||||
console.log(JSON.stringify(config, (k, v) => (k === 'keys' || k === 'private' || k === 'secret') ? undefined : v, 2));
|
||||
cb();
|
||||
};
|
||||
|
||||
|
|
@ -182,10 +304,51 @@ if (argv[0] === 'start') {
|
|||
} catch (_) {}
|
||||
}, 1000);
|
||||
|
||||
const ownFeedSeq = () => new Promise((res) => {
|
||||
try {
|
||||
const pull = require('pull-stream');
|
||||
pull(
|
||||
server.createUserStream({ id: server.id, reverse: true, limit: 1 }),
|
||||
pull.collect((err, msgs) => res(err || !msgs || !msgs.length ? 0 : (msgs[0].value && msgs[0].value.sequence) || 0))
|
||||
);
|
||||
} catch (_) { res(0); }
|
||||
});
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
if (!(await ownFeedSeq())) return;
|
||||
const pull = require('pull-stream');
|
||||
const version = String(require('./package.json').version || '');
|
||||
if (!version) return;
|
||||
pull(
|
||||
server.createUserStream({ id: server.id, reverse: true }),
|
||||
pull.filter(m => m && m.value && m.value.content && m.value.content.type === 'oasisVersion'),
|
||||
pull.take(1),
|
||||
pull.collect((err, msgs) => {
|
||||
const mine = !err && msgs && msgs.length ? msgs[0].value.content.version : null;
|
||||
if (mine !== version) {
|
||||
try { server.publish({ type: 'oasisVersion', version, updatedAt: new Date().toISOString() }, () => {}); } catch (_) {}
|
||||
}
|
||||
})
|
||||
);
|
||||
} catch (_) {}
|
||||
}, 7000);
|
||||
|
||||
};
|
||||
|
||||
if (argv[0] === 'start') {
|
||||
migrateFlume().then(startServerOnly, handleFatal);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
config,
|
||||
server: server || Server(config),
|
||||
open: async () => server || Server(config)
|
||||
get server() {
|
||||
return server;
|
||||
},
|
||||
migrationStatus: () => ({ ...migration }),
|
||||
open: async () => {
|
||||
await migrateFlume();
|
||||
if (!server) { tightenStateDir(); server = Server(config); }
|
||||
return server;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
651
src/server/db2_legacy.js
Normal file
651
src/server/db2_legacy.js
Normal file
|
|
@ -0,0 +1,651 @@
|
|||
const pull = require('pull-stream');
|
||||
const cat = require('pull-cat');
|
||||
const defer = require('pull-defer');
|
||||
const pl = require('pull-level');
|
||||
const bipf = require('bipf');
|
||||
const ssbKeys = require('ssb-keys');
|
||||
const ssbMsgs = require('ssb-msgs');
|
||||
const { heads } = require('ssb-sort');
|
||||
const Plugin = require('ssb-db2/indexes/plugin');
|
||||
const { where, and, or, type, author, descending, sortByArrival, toPullStream, toCallback, startFrom, paginate, seqs, isDecrypted, batch, lt, lte } = require('ssb-db2/operators');
|
||||
|
||||
const BIPF_CONTENT = bipf.allocAndEncode('content');
|
||||
const BIPF_AUTHOR = bipf.allocAndEncode('author');
|
||||
const BIPF_TIMESTAMP = bipf.allocAndEncode('timestamp');
|
||||
const BIPF_TYPE = bipf.allocAndEncode('type');
|
||||
const LINK_RE = /^[@%&]/;
|
||||
const SEARCH_LIMIT = 100;
|
||||
const ARRIVAL_SLACK_MS = 10 * 60 * 1000;
|
||||
const ACTIVITY_DRAIN_MS = 3000;
|
||||
const RECENT_KEEP = 64;
|
||||
const FIRST_PAGE = 1000;
|
||||
const LAST_PAGE = 64000;
|
||||
const OPS = ['$gt', '$gte', '$lt', '$lte', '$in', '$is', '$prefix', '$ne', '$truthy', '$eq'];
|
||||
|
||||
class LinksIndex extends Plugin {
|
||||
constructor(log, dir) {
|
||||
super(log, dir, 'oasisLinks', 1, 'json', 'json');
|
||||
}
|
||||
|
||||
processRecord(record, seq, pValue) {
|
||||
const buf = record.value;
|
||||
const pContent = bipf.seekKey2(buf, pValue, BIPF_CONTENT, 0);
|
||||
if (pContent < 0) return;
|
||||
const content = bipf.decode(buf, pContent);
|
||||
if (!content || typeof content !== 'object') return;
|
||||
const pAuthor = bipf.seekKey2(buf, pValue, BIPF_AUTHOR, 0);
|
||||
const source = pAuthor < 0 ? null : bipf.decode(buf, pAuthor);
|
||||
const seen = new Set();
|
||||
ssbMsgs.indexLinks(content, (link, rel) => {
|
||||
const dest = link && link.link;
|
||||
if (typeof dest !== 'string' || !LINK_RE.test(dest)) return;
|
||||
const mark = `${dest}\u0000${rel}`;
|
||||
if (seen.has(mark)) return;
|
||||
seen.add(mark);
|
||||
this.batch.push({ type: 'put', key: [dest, seq, rel], value: { s: seq, a: source, r: rel } });
|
||||
});
|
||||
}
|
||||
|
||||
indexesContent() {
|
||||
return true;
|
||||
}
|
||||
|
||||
getLinks(dest, cb) {
|
||||
pull(
|
||||
pl.read(this.level, { gte: [dest, ''], lte: [dest, undefined], keyEncoding: this.keyEncoding, valueEncoding: this.valueEncoding, keys: false }),
|
||||
pull.collect((err, arr) => err ? cb(err) : cb(null, (arr || []).filter(Boolean)))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActivityIndex extends Plugin {
|
||||
constructor(log, dir) {
|
||||
super(log, dir, 'oasisActivity', 2, undefined, 'json');
|
||||
this.latest = new Map();
|
||||
this.dirty = new Set();
|
||||
this.reached = -1;
|
||||
this.waiting = [];
|
||||
this.recent = [];
|
||||
const onRecord = this.onRecord;
|
||||
this.onRecord = (record, isLive, pValue) => {
|
||||
onRecord.call(this, record, isLive, pValue);
|
||||
const last = this.recent.length ? this.recent[this.recent.length - 1] : -1;
|
||||
if (record.value && record.offset > last) {
|
||||
this.recent.push(record.offset);
|
||||
if (this.recent.length > RECENT_KEEP) this.recent.shift();
|
||||
}
|
||||
this.reach(record.offset);
|
||||
};
|
||||
this.offset((offset) => this.reach(offset));
|
||||
}
|
||||
|
||||
reach(offset) {
|
||||
if (!(offset > this.reached)) return;
|
||||
this.reached = offset;
|
||||
if (!this.waiting.length) return;
|
||||
const due = this.waiting.filter((w) => w.offset <= offset);
|
||||
this.waiting = this.waiting.filter((w) => w.offset > offset);
|
||||
for (const w of due) w.done();
|
||||
}
|
||||
|
||||
whenReached(offset, ms, done) {
|
||||
if (this.reached >= offset) return done(true);
|
||||
let settled = false;
|
||||
const finish = (ok) => { if (settled) return; settled = true; clearTimeout(timer); done(ok); };
|
||||
const timer = setTimeout(() => { this.waiting = this.waiting.filter((w) => w.done !== hit); finish(false); }, ms);
|
||||
const hit = () => finish(true);
|
||||
this.waiting.push({ offset, done: hit });
|
||||
}
|
||||
|
||||
onLoaded(cb) {
|
||||
pull(
|
||||
pl.read(this.level, { gt: '\x00', valueEncoding: this.valueEncoding }),
|
||||
pull.drain(({ key, value }) => { this.latest.set(key, value); }, (err) => cb(err && err !== true ? err : undefined))
|
||||
);
|
||||
}
|
||||
|
||||
processRecord(record, seq, pValue) {
|
||||
const buf = record.value;
|
||||
const pAuthor = bipf.seekKey2(buf, pValue, BIPF_AUTHOR, 0);
|
||||
if (pAuthor < 0) return;
|
||||
const author = bipf.decode(buf, pAuthor);
|
||||
if (typeof author !== 'string') return;
|
||||
const pContent = bipf.seekKey2(buf, pValue, BIPF_CONTENT, 0);
|
||||
if (pContent >= 0 && bipf.getEncodedType(buf, pContent) === bipf.types.object) {
|
||||
const pType = bipf.seekKey2(buf, pContent, BIPF_TYPE, 0);
|
||||
if (pType >= 0 && bipf.decode(buf, pType) === 'tombstone') return;
|
||||
}
|
||||
const pDeclared = bipf.seekKey2(buf, pValue, BIPF_TIMESTAMP, 0);
|
||||
const pArrival = bipf.seekKey2(buf, 0, BIPF_TIMESTAMP, 0);
|
||||
let ts = Number(pDeclared >= 0 ? bipf.decode(buf, pDeclared) : 0) || Number(pArrival >= 0 ? bipf.decode(buf, pArrival) : 0) || 0;
|
||||
if (ts > 0 && ts < 1e12) ts *= 1000;
|
||||
if (!(ts > (this.latest.get(author) || 0))) return;
|
||||
this.latest.set(author, ts);
|
||||
this.dirty.add(author);
|
||||
}
|
||||
|
||||
onFlush(cb) {
|
||||
for (const author of this.dirty) this.batch.push({ type: 'put', key: author, value: this.latest.get(author) });
|
||||
this.dirty.clear();
|
||||
cb();
|
||||
}
|
||||
|
||||
indexesContent() {
|
||||
return false;
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.latest.clear();
|
||||
this.dirty.clear();
|
||||
this.recent = [];
|
||||
}
|
||||
}
|
||||
|
||||
const linksOf = (content) => {
|
||||
const out = [];
|
||||
if (!content || typeof content !== 'object') return out;
|
||||
ssbMsgs.indexLinks(content, (link, rel) => {
|
||||
const dest = link && link.link;
|
||||
if (typeof dest === 'string' && LINK_RE.test(dest)) out.push({ dest, rel });
|
||||
});
|
||||
return out;
|
||||
};
|
||||
|
||||
const plainOf = (kvt, keys, opts) => {
|
||||
if (!kvt || !kvt.value) return null;
|
||||
const base = { key: kvt.key, value: kvt.value, timestamp: kvt.timestamp };
|
||||
const c = kvt.value.content;
|
||||
if (typeof c !== 'string') return opts.decryptedOnly ? null : base;
|
||||
if (!opts.private && !opts.decryptedOnly) return base;
|
||||
let plain = null;
|
||||
try { plain = ssbKeys.unbox(c, keys.private); } catch (_) { plain = null; }
|
||||
if (!plain) return opts.decryptedOnly ? null : base;
|
||||
return legacy({ key: kvt.key, value: { ...kvt.value, content: plain }, timestamp: kvt.timestamp, meta: { private: true, originalContent: c } }, { private: opts.private });
|
||||
};
|
||||
|
||||
const liveTap = (sbot, keys, opts, match) => {
|
||||
const items = [];
|
||||
let waiting = null;
|
||||
let ended = null;
|
||||
const remove = sbot.db.onMsgAdded((ev) => {
|
||||
if (ended) return false;
|
||||
const m = plainOf(ev && ev.kvt, keys, opts);
|
||||
if (!m || (match && !match(m))) return;
|
||||
items.push(m);
|
||||
if (waiting) { const w = waiting; waiting = null; w(null, items.shift()); }
|
||||
}, false);
|
||||
return (abort, cb) => {
|
||||
if (abort) {
|
||||
if (!ended) { ended = abort; items.length = 0; remove(); }
|
||||
return cb(abort);
|
||||
}
|
||||
if (items.length) return cb(null, items.shift());
|
||||
if (ended) return cb(ended);
|
||||
waiting = cb;
|
||||
};
|
||||
};
|
||||
|
||||
const withSync = (old$, live$, opts = {}) => {
|
||||
const seen = new Set();
|
||||
const parts = [];
|
||||
if (opts.old !== false) parts.push(pull(old$, pull.through((m) => { if (m && m.key) seen.add(m.key); })));
|
||||
if (opts.sync !== false) parts.push(pull.values([{ sync: true }]));
|
||||
parts.push(pull(live$, pull.filter((m) => !(m && m.key && seen.has(m.key)))));
|
||||
return cat(parts);
|
||||
};
|
||||
|
||||
const legacy = (msg, opts = {}) => {
|
||||
if (!msg || !msg.value || msg.sync) return msg;
|
||||
const priv = msg.meta && msg.meta.private === true;
|
||||
if (!priv) return { key: msg.key, value: msg.value, timestamp: msg.timestamp };
|
||||
if (!opts.private) return { key: msg.key, value: { ...msg.value, content: msg.meta.originalContent }, timestamp: msg.timestamp };
|
||||
return {
|
||||
key: msg.key,
|
||||
value: { ...msg.value, private: true, meta: { ...(msg.value.meta || {}), private: true, original: { content: msg.meta.originalContent } } },
|
||||
timestamp: msg.timestamp
|
||||
};
|
||||
};
|
||||
|
||||
const isOpObject = (f) => f && typeof f === 'object' && !Array.isArray(f) && Object.keys(f).some(k => OPS.includes(k));
|
||||
|
||||
const matchValue = (v, f) => {
|
||||
if (isOpObject(f)) {
|
||||
for (const op of Object.keys(f)) {
|
||||
const arg = f[op];
|
||||
if (op === '$gt' && !(v > arg)) return false;
|
||||
if (op === '$gte' && !(v >= arg)) return false;
|
||||
if (op === '$lt' && !(v < arg)) return false;
|
||||
if (op === '$lte' && !(v <= arg)) return false;
|
||||
if (op === '$in' && !(Array.isArray(arg) && arg.includes(v))) return false;
|
||||
if (op === '$is' && typeof v !== arg) return false;
|
||||
if (op === '$prefix' && !(typeof v === 'string' && v.startsWith(arg))) return false;
|
||||
if (op === '$ne' && v === arg) return false;
|
||||
if (op === '$truthy' && !v) return false;
|
||||
if (op === '$eq' && v !== arg) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (f && typeof f === 'object' && !Array.isArray(f)) {
|
||||
if (!v || typeof v !== 'object') return false;
|
||||
return Object.keys(f).every(k => matchValue(v[k], f[k]));
|
||||
}
|
||||
if (Array.isArray(f)) return JSON.stringify(v) === JSON.stringify(f);
|
||||
return v === f;
|
||||
};
|
||||
|
||||
const formatter = (opts = {}) => {
|
||||
if (opts.keys === false && opts.values !== false) return (m) => (m && m.sync) ? m : m.value;
|
||||
if (opts.values === false) return (m) => (m && m.sync) ? m : m.key;
|
||||
return (m) => m;
|
||||
};
|
||||
|
||||
const matchFilter = (msg, filter) => {
|
||||
if (!filter) return true;
|
||||
return Object.keys(filter).every(k => k === 'dest' ? true : matchValue(msg[k], filter[k]));
|
||||
};
|
||||
|
||||
const opsFor = (filter) => {
|
||||
const ops = [];
|
||||
const value = filter && filter.value;
|
||||
const content = value && value.content;
|
||||
const t = content && content.type;
|
||||
if (typeof t === 'string') ops.push(type(t));
|
||||
else if (t && Array.isArray(t.$in) && t.$in.length) ops.push(or(...t.$in.map(x => type(x))));
|
||||
if (value && typeof value.author === 'string') ops.push(author(value.author));
|
||||
if (!ops.length) return null;
|
||||
return ops.length === 1 ? ops[0] : and(...ops);
|
||||
};
|
||||
|
||||
const filterOf = (filter) => filter ? (m) => matchFilter(m, filter) : null;
|
||||
|
||||
module.exports = [
|
||||
{
|
||||
manifest: {
|
||||
createUserStream: 'source',
|
||||
messagesByType: 'source',
|
||||
links: 'source',
|
||||
createLogStream: 'source',
|
||||
get: 'async',
|
||||
whoami: 'sync',
|
||||
status: 'sync',
|
||||
progress: 'sync',
|
||||
rebuild: 'async'
|
||||
},
|
||||
init(sbot, config) {
|
||||
sbot.db.registerIndex(LinksIndex);
|
||||
const linksIndex = () => sbot.db.getIndex('oasisLinks');
|
||||
|
||||
const pages = (ops, extra) => {
|
||||
let seq = 0;
|
||||
let size = FIRST_PAGE;
|
||||
let total = Infinity;
|
||||
let ended = false;
|
||||
const next = (end, cb) => {
|
||||
if (end) return cb(end);
|
||||
if (ended || seq >= total) return cb(true);
|
||||
sbot.db.query(...[ops ? where(ops) : null, ...extra, startFrom(seq), paginate(size), toCallback((err, answer) => {
|
||||
if (err) return cb(err);
|
||||
if (!answer || !answer.total || !answer.results || !answer.results.length) { ended = true; return cb(true); }
|
||||
total = answer.total;
|
||||
seq = answer.nextSeq;
|
||||
size = Math.min(size * 2, LAST_PAGE);
|
||||
cb(null, answer.results);
|
||||
})].filter(Boolean));
|
||||
};
|
||||
return pull(next, pull.map((page) => pull.values(page)), pull.flatten());
|
||||
};
|
||||
|
||||
const query = (ops, extra, size) => size
|
||||
? sbot.db.query(...[ops ? where(ops) : null, ...extra, batch(size), toPullStream()].filter(Boolean))
|
||||
: pages(ops, extra);
|
||||
|
||||
const stream = (ops, opts = {}) => {
|
||||
const shape = () => pull.map(m => legacy(m, { private: opts.private }));
|
||||
const boxed = () => opts.skipPrivate ? pull.filter(m => m.sync || typeof (m.value && m.value.content) !== 'string') : pull.through();
|
||||
const pass = () => opts.filter ? pull.filter(opts.filter) : pull.through();
|
||||
const limit = typeof opts.limit === 'number' && opts.limit >= 0 ? opts.limit : -1;
|
||||
const order = [opts.arrival ? sortByArrival() : null, opts.reverse ? descending() : null].filter(Boolean);
|
||||
const fmt = formatter(opts);
|
||||
const size = limit >= 0 ? Math.max(1, Math.min(limit, FIRST_PAGE)) : undefined;
|
||||
if (!opts.live) return pull(query(ops, order, size), shape(), boxed(), pass(), limit >= 0 ? pull.take(limit) : pull.through(), pull.map(fmt));
|
||||
const match = (m) => (!opts.skipPrivate || typeof (m.value && m.value.content) !== 'string') && (!opts.match || opts.match(m)) && (!opts.filter || opts.filter(m));
|
||||
const live$ = liveTap(sbot, config.keys, { private: opts.private, decryptedOnly: opts.decryptedOnly }, match);
|
||||
const old$ = pull(query(ops, order, size), shape(), boxed(), pass(), limit >= 0 ? pull.take(limit) : pull.through());
|
||||
return pull(withSync(old$, live$, opts), pull.map(fmt));
|
||||
};
|
||||
|
||||
const bySeqs = (list, opts = {}) => {
|
||||
const unique = [...new Set(list)];
|
||||
if (!unique.length) return pull.empty();
|
||||
return pull(query(seqs(unique), []), pull.map(m => legacy(m, { private: opts.private })));
|
||||
};
|
||||
|
||||
const linked = (dest, opts, cb) => {
|
||||
sbot.db.onDrain('oasisLinks', () => {
|
||||
const idx = linksIndex();
|
||||
if (!idx) return cb(null, []);
|
||||
idx.getLinks(dest, (err, entries) => {
|
||||
if (err) return cb(err);
|
||||
const wanted = entries.filter(e => (!opts.rel || e.r === opts.rel) && (!opts.source || e.a === opts.source));
|
||||
pull(bySeqs(wanted.map(e => e.s), opts), pull.collect(cb));
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const liveLinked = (dest, opts) => liveTap(sbot, config.keys, { private: opts.private }, (m) =>
|
||||
linksOf(m.value && m.value.content).some(l => l.dest === dest && (!opts.rel || l.rel === opts.rel)) && (!opts.source || m.value.author === opts.source));
|
||||
|
||||
const sortLegacy = (arr, byTs, reverse) => {
|
||||
const claimed = (m) => Number(m.value && m.value.timestamp) || 0;
|
||||
const arrival = (m) => Number(m.timestamp) || 0;
|
||||
const seq = (m) => Number(m.value && m.value.sequence) || 0;
|
||||
arr.sort((a, b) => (byTs ? (claimed(a) - claimed(b)) : 0) || (arrival(a) - arrival(b)) || (seq(a) - seq(b)));
|
||||
if (reverse) arr.reverse();
|
||||
return arr;
|
||||
};
|
||||
|
||||
let drained = false;
|
||||
try { sbot.db.onDrain('base', () => { drained = true; }); } catch (_) { drained = true; }
|
||||
const status = () => {
|
||||
const st = (sbot.db.getStatus && sbot.db.getStatus().value) || { log: 0, jit: {}, indexes: {}, progress: 1 };
|
||||
return {
|
||||
ready: drained,
|
||||
sync: { since: Number(st.log) || 0, plugins: { ...(st.indexes || {}) } },
|
||||
jit: { ...(st.jit || {}) },
|
||||
progress: typeof st.progress === 'number' ? st.progress : 1,
|
||||
db: 'ssb-db2'
|
||||
};
|
||||
};
|
||||
|
||||
const progress = () => {
|
||||
const st = status();
|
||||
const target = st.sync.since;
|
||||
return { indexes: { start: 0, current: Math.min(target, Math.round(st.progress * target)), target } };
|
||||
};
|
||||
|
||||
const rebuild = (cb) => {
|
||||
if (typeof cb === 'function') return sbot.db.reset(cb);
|
||||
return new Promise((resolve, reject) => sbot.db.reset((err) => err ? reject(err) : resolve()));
|
||||
};
|
||||
|
||||
const createUserStream = (opts = {}) => {
|
||||
const id = typeof opts === 'string' ? opts : opts.id;
|
||||
if (!id) return pull.error(new Error('createUserStream: missing id'));
|
||||
const seqOf = (m) => Number(m.value && m.value.sequence) || 0;
|
||||
const filter = (opts.gt !== undefined || opts.gte !== undefined || opts.lt !== undefined || opts.lte !== undefined)
|
||||
? (m) => (opts.gt === undefined || seqOf(m) > opts.gt) && (opts.gte === undefined || seqOf(m) >= opts.gte) && (opts.lt === undefined || seqOf(m) < opts.lt) && (opts.lte === undefined || seqOf(m) <= opts.lte)
|
||||
: null;
|
||||
return stream(author(id), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, filter, match: (m) => m.value && m.value.author === id, private: opts.private === true, arrival: true, keys: opts.keys, values: opts.values });
|
||||
};
|
||||
|
||||
const messagesByType = (opts = {}) => {
|
||||
const t = typeof opts === 'string' ? opts : opts.type;
|
||||
if (!t) return pull.error(new Error('messagesByType: missing type'));
|
||||
const o = typeof opts === 'string' ? {} : opts;
|
||||
return stream(type(t), { reverse: o.reverse === true, limit: o.limit, live: o.live === true, old: o.old, sync: o.sync, skipPrivate: true, match: (m) => !!(m.value && m.value.content && m.value.content.type === t), arrival: true, keys: o.keys, values: o.values });
|
||||
};
|
||||
|
||||
const links = (opts = {}) => {
|
||||
const src = defer.source();
|
||||
const toLink = (m) => opts.values ? m : { source: m.value.author, dest: opts.dest, rel: opts.rel, key: m.key };
|
||||
if (opts.dest) {
|
||||
const live$ = opts.live ? liveLinked(opts.dest, { rel: opts.rel, source: opts.source }) : null;
|
||||
linked(opts.dest, { rel: opts.rel, source: opts.source }, (err, arr) => {
|
||||
if (err) { if (live$) live$(err, () => {}); return src.abort(err); }
|
||||
const out = sortLegacy(arr, false, opts.reverse === true);
|
||||
if (!live$) return src.resolve(pull(pull.values(out), pull.map(toLink)));
|
||||
src.resolve(pull(withSync(pull.values(out), live$, opts), pull.map((m) => m.sync ? m : toLink(m))));
|
||||
});
|
||||
return src;
|
||||
}
|
||||
const ops = opts.source ? author(opts.source) : null;
|
||||
src.resolve(pull(
|
||||
stream(ops, { reverse: opts.reverse === true, arrival: true }),
|
||||
pull.map(m => linksOf(m.value && m.value.content).filter(l => !opts.rel || l.rel === opts.rel).map(l => opts.values ? m : { source: m.value.author, dest: l.dest, rel: l.rel, key: m.key })),
|
||||
pull.flatten()
|
||||
));
|
||||
return src;
|
||||
};
|
||||
|
||||
const createLogStream = (opts = {}) => {
|
||||
const bound = (k) => opts[k] !== undefined && opts[k] !== null && Number.isFinite(Number(opts[k]));
|
||||
const upper = [bound('lt') ? lt(Number(opts.lt), 'timestamp') : null, bound('lte') ? lte(Number(opts.lte), 'timestamp') : null].filter(Boolean);
|
||||
const upperOps = upper.length ? (upper.length === 1 ? upper[0] : and(...upper)) : null;
|
||||
const hasRange = ['gt', 'gte', 'lt', 'lte'].some(k => opts[k] !== undefined && opts[k] !== null);
|
||||
const ts = (m) => Number(m && m.timestamp) || 0;
|
||||
const inRange = (m) => !m || m.sync || typeof m !== 'object' || (
|
||||
(opts.gt === undefined || opts.gt === null || ts(m) > opts.gt) &&
|
||||
(opts.gte === undefined || opts.gte === null || ts(m) >= opts.gte) &&
|
||||
(opts.lt === undefined || opts.lt === null || ts(m) < opts.lt) &&
|
||||
(opts.lte === undefined || opts.lte === null || ts(m) <= opts.lte)
|
||||
);
|
||||
const lower = Math.max(bound('gt') ? Number(opts.gt) : -Infinity, bound('gte') ? Number(opts.gte) : -Infinity);
|
||||
if (lower > -Infinity && opts.live !== true) {
|
||||
const floor = lower - ARRIVAL_SLACK_MS;
|
||||
const src = defer.source();
|
||||
pull(
|
||||
stream(upperOps, { reverse: true, arrival: true }),
|
||||
pull.take((m) => !m || typeof m !== 'object' || ts(m) >= floor),
|
||||
pull.filter(inRange),
|
||||
pull.collect((err, arr) => {
|
||||
if (err) return src.resolve(pull.error(err));
|
||||
const ordered = opts.reverse === true ? arr : arr.reverse();
|
||||
const limited = opts.limit > 0 ? ordered.slice(0, opts.limit) : ordered;
|
||||
src.resolve(pull.values(limited));
|
||||
})
|
||||
);
|
||||
return src;
|
||||
}
|
||||
const shaped = stream(upperOps, { reverse: opts.reverse === true, limit: hasRange ? undefined : opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, arrival: true, keys: opts.keys, values: opts.values });
|
||||
if (!hasRange) return shaped;
|
||||
const filtered = pull(shaped, pull.filter(inRange));
|
||||
return opts.limit > 0 ? pull(filtered, pull.take(opts.limit)) : filtered;
|
||||
};
|
||||
|
||||
const get = (idOrObject, cb) => {
|
||||
const isObj = !!(idOrObject && typeof idOrObject === 'object');
|
||||
const id = isObj ? idOrObject.id : idOrObject;
|
||||
sbot.db.getMsg(id, (err, msg) => {
|
||||
if (err) return cb(err);
|
||||
const shaped = legacy(msg, { private: isObj && idOrObject.private === true });
|
||||
cb(null, isObj && idOrObject.meta ? shaped : shaped.value);
|
||||
});
|
||||
};
|
||||
|
||||
const whoami = () => ({ id: sbot.id });
|
||||
|
||||
sbot._oasisDb2 = { stream, linked, liveLinked, bySeqs, sortLegacy, legacy, matchFilter, opsFor, filterOf, linksOf };
|
||||
return { createUserStream, messagesByType, links, createLogStream, get, whoami, status, progress, rebuild };
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'private',
|
||||
version: '1.0.0',
|
||||
manifest: { publish: 'async', unbox: 'sync', read: 'source' },
|
||||
init(sbot, config) {
|
||||
return {
|
||||
publish(content, recps, cb) {
|
||||
const list = (Array.isArray(recps) ? recps : [recps]).map(r => typeof r === 'string' ? r : (r && r.link)).filter(Boolean);
|
||||
if (!list.length) return cb(new Error('private.publish: no recipients'));
|
||||
sbot.db.create({ content, recps: list, encryptionFormat: 'box' }, (err, kvt) => err ? cb(err) : cb(null, legacy(kvt)));
|
||||
},
|
||||
unbox(msgOrData) {
|
||||
if (typeof msgOrData === 'string') return ssbKeys.unbox(msgOrData, config.keys.private);
|
||||
if (!msgOrData || !msgOrData.value || typeof msgOrData.value.content !== 'string') return undefined;
|
||||
let plain = null;
|
||||
try { plain = ssbKeys.unbox(msgOrData.value.content, config.keys.private); } catch (_) { plain = null; }
|
||||
if (!plain) return undefined;
|
||||
const v = msgOrData.value;
|
||||
return {
|
||||
key: msgOrData.key,
|
||||
value: { previous: v.previous, author: v.author, sequence: v.sequence, timestamp: v.timestamp, hash: v.hash, content: plain, private: true },
|
||||
timestamp: msgOrData.timestamp
|
||||
};
|
||||
},
|
||||
read(opts = {}) {
|
||||
return sbot._oasisDb2.stream(isDecrypted('box'), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, private: true, decryptedOnly: true, arrival: true, keys: opts.keys, values: opts.values });
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'activity',
|
||||
version: '1.0.0',
|
||||
manifest: { latest: 'async', tail: 'async' },
|
||||
init(sbot) {
|
||||
sbot.db.registerIndex(ActivityIndex);
|
||||
return {
|
||||
latest(ids, opts, cb) {
|
||||
if (typeof ids === 'function') { cb = ids; ids = null; opts = {}; }
|
||||
if (typeof opts === 'function') { cb = opts; opts = {}; }
|
||||
const idx = sbot.db.getIndex('oasisActivity');
|
||||
if (!idx) return cb(null, null);
|
||||
let settled = false;
|
||||
const settle = (ok) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
if (!ok && !(idx.offset.value >= 0)) return cb(null, null);
|
||||
const out = {};
|
||||
if (Array.isArray(ids)) { for (const id of ids) if (idx.latest.has(id)) out[id] = idx.latest.get(id); }
|
||||
else for (const [id, ts] of idx.latest) out[id] = ts;
|
||||
cb(null, out);
|
||||
};
|
||||
const timer = setTimeout(() => settle(false), ACTIVITY_DRAIN_MS);
|
||||
const log = sbot.db.getLog();
|
||||
const read = () => {
|
||||
const since = log.since.value;
|
||||
if (!Number.isFinite(since)) return settle(idx.offset.value >= 0);
|
||||
idx.whenReached(since, ACTIVITY_DRAIN_MS, settle);
|
||||
};
|
||||
if (opts && opts.fresh === false) read();
|
||||
else log.onDrain(read);
|
||||
},
|
||||
tail(limit, opts, cb) {
|
||||
if (typeof limit === 'function') { cb = limit; limit = 1; opts = {}; }
|
||||
if (typeof opts === 'function') { cb = opts; opts = {}; }
|
||||
const want = Math.max(1, Math.min(Number(limit) || 1, RECENT_KEEP));
|
||||
const idx = sbot.db.getIndex('oasisActivity');
|
||||
if (!idx) return cb(null, null);
|
||||
const log = sbot.db.getLog();
|
||||
const read = () => {
|
||||
const since = log.since.value;
|
||||
if (!Number.isFinite(since)) return cb(null, []);
|
||||
idx.whenReached(since, ACTIVITY_DRAIN_MS, (ok) => {
|
||||
const offsets = idx.recent;
|
||||
if (!ok || !offsets.length || offsets[offsets.length - 1] !== since) return cb(null, null);
|
||||
if (offsets.length < want && offsets[0] !== 0) return cb(null, null);
|
||||
const picked = offsets.slice(-want).reverse();
|
||||
const out = new Array(picked.length);
|
||||
let left = picked.length;
|
||||
let failed = false;
|
||||
picked.forEach((offset, i) => log.get(offset, (err, buf) => {
|
||||
if (failed) return;
|
||||
let m = null;
|
||||
try { m = !err && buf ? bipf.decode(buf, 0) : null; } catch (_) { m = null; }
|
||||
if (!m || !m.key || !m.value) { failed = true; return cb(null, null); }
|
||||
out[i] = legacy(m);
|
||||
if (--left === 0) cb(null, out);
|
||||
}));
|
||||
});
|
||||
};
|
||||
if (opts && opts.fresh === false) read();
|
||||
else log.onDrain(read);
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'backlinks',
|
||||
version: '1.0.0',
|
||||
manifest: { read: 'source' },
|
||||
init(sbot) {
|
||||
return {
|
||||
read(opts = {}) {
|
||||
const { stream, linked, liveLinked, sortLegacy, matchFilter, opsFor } = sbot._oasisDb2;
|
||||
const filter = (opts.query && opts.query[0] && opts.query[0].$filter) || {};
|
||||
const priv = opts.private === true;
|
||||
const pass = (m) => matchFilter(m, filter);
|
||||
if (!filter.dest) {
|
||||
return stream(opsFor(filter), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, filter: pass, private: priv });
|
||||
}
|
||||
const src = defer.source();
|
||||
const limit = typeof opts.limit === 'number' && opts.limit >= 0 ? opts.limit : -1;
|
||||
const fmt = formatter(opts);
|
||||
const live$ = opts.live ? pull(liveLinked(filter.dest, { private: priv }), pull.filter(pass)) : null;
|
||||
const finish = (arr) => {
|
||||
let out = sortLegacy(arr.filter(pass), opts.index === 'DTA', opts.reverse === true);
|
||||
if (limit >= 0) out = out.slice(0, limit);
|
||||
if (!live$) return src.resolve(pull(pull.values(out), pull.map(fmt)));
|
||||
src.resolve(pull(withSync(pull.values(out), live$, opts), pull.map(fmt)));
|
||||
};
|
||||
if (opts.old === false && opts.live) return finish([]), src;
|
||||
linked(filter.dest, { private: priv }, (err, arr) => {
|
||||
if (!err) return finish(arr);
|
||||
if (live$) live$(err, () => {});
|
||||
src.abort(err);
|
||||
});
|
||||
return src;
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'query',
|
||||
version: '1.0.0',
|
||||
manifest: { read: 'source' },
|
||||
init(sbot) {
|
||||
return {
|
||||
read(opts = {}) {
|
||||
const { stream, opsFor, filterOf } = sbot._oasisDb2;
|
||||
const filter = (opts.query && opts.query[0] && opts.query[0].$filter) || null;
|
||||
return stream(opsFor(filter), { reverse: opts.reverse === true, limit: opts.limit, live: opts.live === true, old: opts.old, sync: opts.sync, filter: filterOf(filter), skipPrivate: true, keys: opts.keys, values: opts.values });
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'search',
|
||||
version: '1.0.0',
|
||||
manifest: { query: 'source' },
|
||||
init(sbot) {
|
||||
const textOf = (c) => ['text', 'title', 'description', 'body', 'name', 'summary'].map(k => c && typeof c[k] === 'string' ? c[k] : '').join('\n').toLowerCase();
|
||||
return {
|
||||
query(opts = {}) {
|
||||
const needle = String(opts.query || '').trim().toLowerCase();
|
||||
if (!needle) return pull.empty();
|
||||
const terms = needle.split(/\s+/).filter(Boolean);
|
||||
const limit = typeof opts.limit === 'number' && opts.limit > 0 ? opts.limit : SEARCH_LIMIT;
|
||||
return sbot._oasisDb2.stream(null, {
|
||||
reverse: opts.reverse !== false,
|
||||
limit,
|
||||
skipPrivate: true,
|
||||
arrival: true,
|
||||
filter: (m) => { const c = m.value && m.value.content; if (!c || typeof c !== 'object') return false; const hay = textOf(c); return terms.every(t => hay.includes(t)); }
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'tangle',
|
||||
version: '1.0.0',
|
||||
manifest: { branch: 'async' },
|
||||
init(sbot) {
|
||||
return {
|
||||
branch(rootKey, cb) {
|
||||
if (typeof rootKey !== 'string' || !rootKey.startsWith('%')) return cb(new Error('tangle.branch requires a message key'));
|
||||
sbot._oasisDb2.linked(rootKey, {}, (err, arr) => {
|
||||
if (err) return cb(err);
|
||||
const thread = arr.filter(m => m.value && m.value.content && typeof m.value.content === 'object' && m.value.content.root === rootKey);
|
||||
cb(null, heads([{ key: rootKey }, ...thread]));
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
module.exports.LinksIndex = LinksIndex;
|
||||
module.exports.legacy = legacy;
|
||||
module.exports.matchFilter = matchFilter;
|
||||
97
src/server/lanRouter.js
Normal file
97
src/server/lanRouter.js
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
const pull = require('./node_modules/pull-stream');
|
||||
const Ref = require('./node_modules/ssb-ref');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const staged = new Set();
|
||||
|
||||
function readOasisConfig() {
|
||||
try {
|
||||
const dir = process.env.OASIS_STATE_DIR || process.env.ssb_path || path.join(require('os').homedir(), '.ssb');
|
||||
const p = [path.join(dir, 'oasis', 'oasis-config.json'), path.join(dir, 'oasis-config.json')].find(f => fs.existsSync(f));
|
||||
return JSON.parse(fs.readFileSync(p || path.join(__dirname, '..', 'configs', 'oasis-config.json'), 'utf8')) || {};
|
||||
} catch (_) { return {}; }
|
||||
}
|
||||
|
||||
function stagePeer(ssb, address, key, eagerReplicate) {
|
||||
if (!address || !key || key === ssb.id) return;
|
||||
if (staged.has(address)) return;
|
||||
staged.add(address);
|
||||
let routed = false;
|
||||
try {
|
||||
if (ssb.conn && typeof ssb.conn.stage === 'function') {
|
||||
ssb.conn.stage(address, { type: 'lan', key });
|
||||
routed = true;
|
||||
}
|
||||
} catch (_) {}
|
||||
try { require('../models/viewer_filters').rememberLanPeers([key]); } catch (_) {}
|
||||
if (!routed) {
|
||||
try {
|
||||
if (ssb.gossip && typeof ssb.gossip.add === 'function') {
|
||||
ssb.gossip.add(address, 'local');
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
if (eagerReplicate) {
|
||||
try {
|
||||
if (ssb.ebt && typeof ssb.ebt.request === 'function') {
|
||||
ssb.ebt.request(key, true);
|
||||
}
|
||||
} catch (_) {}
|
||||
try {
|
||||
if (ssb.replicate && typeof ssb.replicate.request === 'function') {
|
||||
ssb.replicate.request(key, true);
|
||||
}
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
function handleDiscovery(ssb, d, opts) {
|
||||
if (!d || !d.address) return;
|
||||
if (!d.verified && !opts.acceptUnverified) return;
|
||||
let key = null;
|
||||
try { key = Ref.getKeyFromAddress(d.address); } catch (_) {}
|
||||
if (key) stagePeer(ssb, d.address, key, opts.eagerReplicate);
|
||||
}
|
||||
|
||||
function guardLan(ssb, isEnabled) {
|
||||
const lan = ssb && ssb.lan;
|
||||
if (!lan || typeof lan.start !== 'function' || lan.oasisGuarded) return false;
|
||||
const start = lan.start.bind(lan);
|
||||
lan.start = (...args) => {
|
||||
if (!isEnabled()) return undefined;
|
||||
try { return start(...args); } catch (_) { return undefined; }
|
||||
};
|
||||
lan.oasisGuarded = true;
|
||||
if (!isEnabled() && typeof lan.stop === 'function') { try { lan.stop(); } catch (_) {} }
|
||||
return true;
|
||||
}
|
||||
|
||||
function startRouter(ssb, opts, isEnabled) {
|
||||
if (!ssb.lan || typeof ssb.lan.discoveredPeers !== 'function') return;
|
||||
if (isEnabled()) { try { ssb.lan.start(); } catch (_) {} }
|
||||
else if (typeof ssb.lan.stop === 'function') { try { ssb.lan.stop(); } catch (_) {} }
|
||||
pull(
|
||||
ssb.lan.discoveredPeers(),
|
||||
pull.drain(d => handleDiscovery(ssb, d, opts), () => {})
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
name: 'lanRouter',
|
||||
version: '1.1.0',
|
||||
manifest: {},
|
||||
init(ssb, config) {
|
||||
const lanCfg = (config && config.lan) || {};
|
||||
const opts = {
|
||||
acceptUnverified: lanCfg.acceptUnverified === true,
|
||||
eagerReplicate: lanCfg.eagerReplicate === true
|
||||
};
|
||||
const isEnabled = () => { const c = readOasisConfig(); return c.lanBroadcasting !== false && c.networkPaused !== true; };
|
||||
guardLan(ssb, isEnabled);
|
||||
setImmediate(() => startRouter(ssb, opts, isEnabled));
|
||||
return {};
|
||||
}
|
||||
};
|
||||
|
||||
module.exports.guardLan = guardLan;
|
||||
210
src/server/network_pause.js
Normal file
210
src/server/network_pause.js
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const readOasisConfig = () => {
|
||||
try {
|
||||
const dir = process.env.OASIS_STATE_DIR || process.env.ssb_path || path.join(require('os').homedir(), '.ssb');
|
||||
const p = [path.join(dir, 'oasis', 'oasis-config.json'), path.join(dir, 'oasis-config.json')].find(f => fs.existsSync(f));
|
||||
return JSON.parse(fs.readFileSync(p || path.join(__dirname, '..', 'configs', 'oasis-config.json'), 'utf8')) || {};
|
||||
} catch (_) { return {}; }
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
name: 'networkPause',
|
||||
version: '1.0.0',
|
||||
manifest: { pause: 'sync', resume: 'sync', paused: 'sync', lastErrors: 'sync' },
|
||||
init(server, config) {
|
||||
server.oasisNetworkPaused = readOasisConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1';
|
||||
const refused = () => new Error('network paused');
|
||||
|
||||
if (server.auth && typeof server.auth.hook === 'function') {
|
||||
server.auth.hook(function (fn, args) {
|
||||
const cb = args[args.length - 1];
|
||||
if (server.oasisNetworkPaused && args[0] !== server.id && typeof cb === 'function') return cb(refused());
|
||||
return fn.apply(this, args);
|
||||
});
|
||||
}
|
||||
if (server.connect && typeof server.connect.hook === 'function') {
|
||||
server.connect.hook(function (fn, args) {
|
||||
const cb = args[args.length - 1];
|
||||
if (server.oasisNetworkPaused && typeof cb === 'function') return cb(refused());
|
||||
return fn.apply(this, args);
|
||||
});
|
||||
}
|
||||
|
||||
const closeAll = () => {
|
||||
try { for (const [addr] of server.conn.query().peersInConnection()) server.conn.disconnect(addr, () => {}); } catch (_) {}
|
||||
for (const id of Object.keys(server.peers || {})) {
|
||||
if (id === server.id) continue;
|
||||
for (const rpc of (server.peers[id] || []).slice()) { try { rpc.close(true, () => {}); } catch (_) {} }
|
||||
}
|
||||
};
|
||||
|
||||
const lastErrors = new Map();
|
||||
const keyOf = (ev) => {
|
||||
const { canonicalKey } = require('../models/peer_health');
|
||||
const m = String((ev && ev.address) || '').match(/~shs:([A-Za-z0-9+/=_-]{43,44})/);
|
||||
return canonicalKey((ev && ev.key) || (m ? m[1] : ''));
|
||||
};
|
||||
const majorMinor = (v) => String(v || '').split('.').slice(0, 2).map(n => parseInt(n, 10) || 0);
|
||||
const ownVersion = (() => { try { return String(require('./package.json').version || ''); } catch (_) { return ''; } })();
|
||||
const upToDate = (v) => {
|
||||
const [a1, b1] = majorMinor(v), [a2, b2] = majorMinor(ownVersion);
|
||||
return a1 > a2 || (a1 === a2 && b1 >= b2);
|
||||
};
|
||||
const rank = { versions: new Map(), graph: {} };
|
||||
const refreshRank = () => {
|
||||
const pull = require('pull-stream');
|
||||
const latest = new Map();
|
||||
const done = () => {
|
||||
rank.versions = new Map([...latest].map(([k, m]) => [k, m.version]));
|
||||
try { server.friends.graph((err, g) => { if (!err && g) rank.graph = g; }); } catch (_) {}
|
||||
};
|
||||
try {
|
||||
if (typeof server.messagesByType !== 'function') return done();
|
||||
pull(server.messagesByType({ type: 'oasisVersion' }), pull.drain((m) => {
|
||||
const author = m && m.value && m.value.author;
|
||||
const c = m && m.value && m.value.content;
|
||||
if (!author || !c || typeof c.version !== 'string') return;
|
||||
const ts = Number(m.value.timestamp) || 0;
|
||||
const prev = latest.get(author);
|
||||
if (!prev || prev.ts <= ts) latest.set(author, { version: c.version, ts });
|
||||
}, done));
|
||||
} catch (_) { done(); }
|
||||
};
|
||||
const peerRank = (peer) => {
|
||||
const key = peer && peer[1] && peer[1].key;
|
||||
if (!key) return 0;
|
||||
const follows = rank.graph[key] || {};
|
||||
const mine = rank.graph[server.id] || {};
|
||||
let total = 0, shared = 0;
|
||||
for (const [k, v] of Object.entries(follows)) {
|
||||
if (!(v >= 0) || k === key) continue;
|
||||
total++;
|
||||
if (mine[k] >= 0) shared++;
|
||||
}
|
||||
const v = rank.versions.get(key);
|
||||
const tier = v ? (upToDate(v) ? 2 : 0) : (total > 0 ? 0 : 1);
|
||||
return tier * 1e9 + Math.min(shared, 99999) * 1e4 + Math.min(total, 9999);
|
||||
};
|
||||
server.oasisPeerRank = peerRank;
|
||||
setImmediate(refreshRank);
|
||||
const rankTimer = setInterval(refreshRank, 10 * 60 * 1000);
|
||||
if (rankTimer.unref) rankTimer.unref();
|
||||
const fallbackAt = new Map();
|
||||
const tryOtherAddress = (key, failed, reason) => {
|
||||
if (reason === 'paused' || server.oasisNetworkPaused) return;
|
||||
if (Array.isArray(server.peers && server.peers[key]) && server.peers[key].length) return;
|
||||
const now = Date.now();
|
||||
if (now - (fallbackAt.get(key) || 0) < 10 * 60 * 1000) return;
|
||||
const { canonicalKey } = require('../models/peer_health');
|
||||
let entries = [];
|
||||
try { entries = server.conn.dbPeers(); } catch (_) { return; }
|
||||
const failedOnion = /^onion:/.test(String(failed || ''));
|
||||
const alt = entries.find(([addr, data]) => addr && addr !== failed && data && canonicalKey(data.key) === key && /^onion:/.test(addr) !== failedOnion);
|
||||
if (!alt) return;
|
||||
fallbackAt.set(key, now);
|
||||
try { server.conn.connect(alt[0], { ...(alt[1] || {}) }, () => {}); } catch (_) {}
|
||||
};
|
||||
let listening = false;
|
||||
const listen = () => {
|
||||
if (listening) return;
|
||||
try {
|
||||
const { classifyNetError } = require('../models/peer_health');
|
||||
const hub = server.conn && typeof server.conn.hub === 'function' ? server.conn.hub() : null;
|
||||
const stream = hub && typeof hub.listen === 'function' ? hub.listen() : null;
|
||||
if (!stream) return;
|
||||
listening = true;
|
||||
require('pull-stream')(stream, require('pull-stream').drain((ev) => {
|
||||
const key = ev && keyOf(ev);
|
||||
if (!key) return;
|
||||
if (ev.type === 'connected') lastErrors.delete(key);
|
||||
else if (ev.type === 'connecting-failed') {
|
||||
const reason = classifyNetError(ev.details, ev.address);
|
||||
lastErrors.set(key, { reason, at: Date.now() });
|
||||
if (lastErrors.size > 500) lastErrors.delete(lastErrors.keys().next().value);
|
||||
tryOtherAddress(key, ev.address, reason);
|
||||
}
|
||||
}, () => { listening = false; }));
|
||||
} catch (_) { listening = false; }
|
||||
};
|
||||
const listenTimer = setInterval(() => { listen(); if (listening) clearInterval(listenTimer); }, 1000);
|
||||
if (listenTimer.unref) listenTimer.unref();
|
||||
|
||||
const readList = (file) => { try { const a = JSON.parse(fs.readFileSync(file, 'utf8')); return Array.isArray(a) ? a : []; } catch (_) { return []; } };
|
||||
const syncPeerBook = () => {
|
||||
if (!server.conn || typeof server.conn.dbPeers !== 'function' || !config || !config.path) return;
|
||||
const { canonicalKey } = require('../models/peer_health');
|
||||
let forgottenFile = null;
|
||||
try { forgottenFile = require('../configs/state-manager').statePath('gossip_unfollowed.json'); } catch (_) {}
|
||||
const forgotten = new Set((forgottenFile ? readList(forgottenFile) : []).map(e => e && canonicalKey(e.key)).filter(Boolean));
|
||||
let entries = [];
|
||||
try { entries = server.conn.dbPeers(); } catch (_) { return; }
|
||||
for (const [addr, data] of entries) {
|
||||
const m = String(addr).match(/^(.*~shs:)([A-Za-z0-9+/]{43})$/);
|
||||
if (!m) continue;
|
||||
const padded = `${m[1]}${m[2]}=`;
|
||||
try {
|
||||
if (!server.conn.db().has(padded)) server.conn.remember(padded, { ...(data || {}) });
|
||||
else if (data && data.autoconnect === true && typeof server.conn.db().update === 'function') server.conn.db().update(padded, { autoconnect: true });
|
||||
server.conn.forget(addr);
|
||||
} catch (_) {}
|
||||
}
|
||||
try { entries = server.conn.dbPeers(); } catch (_) { return; }
|
||||
for (const [addr, data] of entries) {
|
||||
const key = data && canonicalKey(data.key);
|
||||
if (key && forgotten.has(key)) { try { server.conn.forget(addr); } catch (_) {} }
|
||||
}
|
||||
const chosen = new Map();
|
||||
for (const p of readList(path.join(config.path, 'gossip.json'))) {
|
||||
const key = p && canonicalKey(p.key);
|
||||
const host = p ? String(p.host || '').trim() : '';
|
||||
if (!key || key === server.id || forgotten.has(key) || !host) continue;
|
||||
const onion = /\.onion$/i.test(host);
|
||||
const prev = chosen.get(key);
|
||||
if (!prev || (prev.onion && !onion)) chosen.set(key, { host, port: Number(p.port) || 8008, onion });
|
||||
}
|
||||
for (const [key, p] of chosen) {
|
||||
const addr = `${p.onion ? 'onion' : 'net'}:${p.host}:${p.port}~shs:${key.slice(1, -8)}`;
|
||||
try { if (!server.conn.db().has(addr)) server.conn.remember(addr, { key, type: 'pub', autoconnect: true }); } catch (_) {}
|
||||
}
|
||||
const seeds = [].concat(config.seeds || [], (config.connections && config.connections.seeds) || []);
|
||||
for (const seed of seeds) {
|
||||
const addr = String(seed || '').trim();
|
||||
const m = addr.match(/~shs:([A-Za-z0-9+/]{43}=?)$/);
|
||||
const key = m && canonicalKey(m[1]);
|
||||
if (!key || key === server.id || forgotten.has(key)) continue;
|
||||
try { if (!server.conn.db().has(addr)) server.conn.remember(addr, { key, type: 'pub', autoconnect: true }); } catch (_) {}
|
||||
}
|
||||
if (config.pub && typeof server.conn.db().update === 'function') {
|
||||
try { entries = server.conn.dbPeers(); } catch (_) { return; }
|
||||
for (const [addr, data] of entries) {
|
||||
const key = data && canonicalKey(data.key);
|
||||
if (!key || key === server.id || forgotten.has(key) || data.type !== 'pub' || data.autoconnect !== false) continue;
|
||||
try { server.conn.db().update(addr, { autoconnect: true }); } catch (_) {}
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const ms of [5000, 30000]) { const t = setTimeout(syncPeerBook, ms); if (t.unref) t.unref(); }
|
||||
const bookTimer = setInterval(syncPeerBook, 10 * 60 * 1000);
|
||||
if (bookTimer.unref) bookTimer.unref();
|
||||
|
||||
return {
|
||||
lastErrors() { return Object.fromEntries(lastErrors); },
|
||||
pause() {
|
||||
server.oasisNetworkPaused = true;
|
||||
try { server.conn.stop(); } catch (_) {}
|
||||
try { if (server.lan && typeof server.lan.stop === 'function') server.lan.stop(); } catch (_) {}
|
||||
closeAll();
|
||||
return true;
|
||||
},
|
||||
resume() {
|
||||
server.oasisNetworkPaused = false;
|
||||
try { server.conn.start(); } catch (_) {}
|
||||
try { if (server.lan && typeof server.lan.start === 'function') server.lan.start(); } catch (_) {}
|
||||
return false;
|
||||
},
|
||||
paused() { return !!server.oasisNetworkPaused; }
|
||||
};
|
||||
}
|
||||
};
|
||||
12944
src/server/package-lock.json
generated
12944
src/server/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@krakenslab/oasis",
|
||||
"version": "0.9.5",
|
||||
"version": "1.3.1",
|
||||
"description": "Oasis - Social Networking Utopia",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
@ -17,27 +17,22 @@
|
|||
"start:backend": "node ../backend/backend.js",
|
||||
"start:ssb": "node SSB_server.js start &",
|
||||
"dev": "npm run dev:backend",
|
||||
"dev:backend": "nodemon",
|
||||
"dev:backend": "npx --yes nodemon@3.1.14",
|
||||
"postinstall": "node ../../scripts/patch-node-modules.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@koa/router": "^13.1.0",
|
||||
"@open-rpc/client-js": "^1.8.1",
|
||||
"@xenova/transformers": "^2.17.2",
|
||||
"abstract-level": "^2.0.1",
|
||||
"archiver": "^7.0.1",
|
||||
"axios": "^1.10.0",
|
||||
"cors": "^2.8.5",
|
||||
"bencode": "^2.0.3",
|
||||
"bipf": "^1.9.0",
|
||||
"chloride": "^2.4.1",
|
||||
"debug": "^4.3.1",
|
||||
"dompurify": "^3.3.1",
|
||||
"env-paths": "^2.2.1",
|
||||
"epidemic-broadcast-trees": "^9.0.4",
|
||||
"express": "^5.1.0",
|
||||
"file-type": "^16.5.4",
|
||||
"highlight.js": "11.0.0",
|
||||
"hyperaxe": "^2.0.1",
|
||||
"ip": "https://registry.npmjs.org/neoip/-/neoip-3.0.0.tgz",
|
||||
"is-svg": "^4.4.0",
|
||||
"jsdom": "^28.0.0",
|
||||
"koa": "^2.7.0",
|
||||
"koa-body": "^6.0.1",
|
||||
|
|
@ -47,77 +42,49 @@
|
|||
"minimist": "^1.2.8",
|
||||
"module-alias": "^2.2.3",
|
||||
"moment": "^2.30.1",
|
||||
"multiblob": "^1.13.0",
|
||||
"multiserver": "^3.3.1",
|
||||
"multiserver-address": "^1.0.1",
|
||||
"muxrpc": "^8.0.0",
|
||||
"muxrpc-validation": "^3.0.2",
|
||||
"muxrpcli": "^3.1.2",
|
||||
"node-llama-cpp": "^3.10.0",
|
||||
"non-private-ip": "^2.2.0",
|
||||
"open": "^8.4.2",
|
||||
"openpgp": "^6.3.0",
|
||||
"packet-stream": "^2.0.6",
|
||||
"packet-stream-codec": "^1.2.0",
|
||||
"pdfjs-dist": "^5.2.133",
|
||||
"opusscript": "^0.1.1",
|
||||
"pretty-ms": "^7.0.1",
|
||||
"pull-abortable": "^4.1.1",
|
||||
"pull-cat": "~1.1.5",
|
||||
"pull-file": "^1.0.0",
|
||||
"pull-many": "~1.0.6",
|
||||
"pull-defer": "^0.2.3",
|
||||
"pull-level": "^2.0.4",
|
||||
"pull-paramap": "^1.2.2",
|
||||
"pull-pushable": "^2.2.0",
|
||||
"pull-sort": "^1.0.2",
|
||||
"pull-stream": "^3.7.0",
|
||||
"punycode.js": "^2.3.1",
|
||||
"qrcode": "^1.5.4",
|
||||
"secret-stack": "^6.3.1",
|
||||
"ssb-about": "^2.0.1",
|
||||
"ssb-autofollow": "^1.1.0",
|
||||
"ssb-backlinks": "^2.1.1",
|
||||
"ssb-blobs": "^2.0.1",
|
||||
"ssb-box": "^1.0.1",
|
||||
"ssb-caps": "^1.0.1",
|
||||
"ssb-classic": "^1.1.0",
|
||||
"ssb-client": "^4.9.0",
|
||||
"ssb-config": "^3.4.4",
|
||||
"ssb-conn": "6.0.3",
|
||||
"ssb-conn-db": "^1.0.5",
|
||||
"ssb-conn-hub": "^1.2.0",
|
||||
"ssb-conn-staging": "^1.0.0",
|
||||
"ssb-db": "^20.4.1",
|
||||
"ssb-device-address": "^1.1.6",
|
||||
"ssb-db2": "^6.3.3",
|
||||
"ssb-ebt": "^9.1.2",
|
||||
"ssb-friend-pub": "^1.0.7",
|
||||
"ssb-friends": "^5.0.0",
|
||||
"ssb-gossip": "^1.1.1",
|
||||
"ssb-invite": "^3.0.3",
|
||||
"ssb-invite-client": "^1.3.3",
|
||||
"ssb-keys": "^8.0.0",
|
||||
"ssb-lan": "^1.0.0",
|
||||
"ssb-legacy-conn": "^1.0.17",
|
||||
"ssb-links": "^3.0.10",
|
||||
"ssb-logging": "^1.0.0",
|
||||
"ssb-markdown": "^3.6.0",
|
||||
"ssb-master": "^1.0.3",
|
||||
"ssb-meme": "^1.1.0",
|
||||
"ssb-mentions": "^0.5.2",
|
||||
"ssb-msgs": "^5.2.0",
|
||||
"ssb-no-auth": "^1.0.0",
|
||||
"ssb-onion": "^1.0.0",
|
||||
"ssb-partial-replication": "^3.0.1",
|
||||
"ssb-plugins": "^1.0.2",
|
||||
"ssb-private": "^1.1.0",
|
||||
"ssb-query": "^2.4.5",
|
||||
"ssb-ref": "^2.16.0",
|
||||
"ssb-replication-scheduler": "^3.0.0",
|
||||
"ssb-search": "^1.3.0",
|
||||
"ssb-server": "file:packages/ssb-server",
|
||||
"ssb-tangle": "^1.0.1",
|
||||
"ssb-sort": "^1.1.3",
|
||||
"ssb-thread-schema": "^1.1.1",
|
||||
"ssb-threads": "^10.0.4",
|
||||
"ssb-unix-socket": "^1.0.0",
|
||||
"ssb-ws": "^6.2.3",
|
||||
"util": "^0.12.5",
|
||||
"ssb-validate": "^4.1.4",
|
||||
"stream-to-pull-stream": "^1.7.3",
|
||||
"telegram": "^2.26.22",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"overrides": {
|
||||
|
|
@ -135,15 +102,14 @@
|
|||
"ssb-ref": "^2.16.0",
|
||||
"secret-stack": "^6.3.1",
|
||||
"ip": "https://registry.npmjs.org/neoip/-/neoip-3.0.0.tgz",
|
||||
"lodash.set": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
||||
"lodash.set": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||
"onnxruntime-node": "1.19.2",
|
||||
"onnxruntime-common": "1.19.2",
|
||||
"onnxruntime-web": "1.19.2"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "^2.3.2",
|
||||
"sharp": "^0.33.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.1.0"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/epsylon/oasis/issues"
|
||||
},
|
||||
|
|
@ -153,7 +119,7 @@
|
|||
},
|
||||
"keywords": [],
|
||||
"engines": {
|
||||
"node": "^10.0.0 || >=12.0.0"
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e"
|
||||
}
|
||||
|
|
|
|||
45
src/server/phone_audio_android.js
Normal file
45
src/server/phone_audio_android.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
const fs = require('fs');
|
||||
const net = require('net');
|
||||
const path = require('path');
|
||||
|
||||
const MARKER = path.join(__dirname, '..', '..', '.audio_bridge');
|
||||
const MAX_BACKLOG = 4000;
|
||||
|
||||
const bridgePort = () => {
|
||||
try {
|
||||
const port = Number(String(fs.readFileSync(MARKER, 'utf8')).trim());
|
||||
return Number.isFinite(port) && port > 0 ? port : 0;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = () => {
|
||||
const port = bridgePort();
|
||||
if (!port) return null;
|
||||
const open = (mode) => {
|
||||
const sock = net.connect(port, '127.0.0.1');
|
||||
sock.on('error', () => {});
|
||||
sock.setNoDelay(true);
|
||||
sock.write(mode);
|
||||
return sock;
|
||||
};
|
||||
return {
|
||||
capture(onPcm) {
|
||||
const sock = open('R');
|
||||
sock.on('data', onPcm);
|
||||
return { stop() { try { sock.destroy(); } catch (_) {} } };
|
||||
},
|
||||
player() {
|
||||
const sock = open('P');
|
||||
return {
|
||||
write(buf) {
|
||||
if (sock.destroyed || sock.writableLength > MAX_BACKLOG) return false;
|
||||
sock.write(buf);
|
||||
return true;
|
||||
},
|
||||
stop() { try { sock.end(); sock.destroy(); } catch (_) {} }
|
||||
};
|
||||
}
|
||||
};
|
||||
};
|
||||
1855
src/server/phone_module.js
Normal file
1855
src/server/phone_module.js
Normal file
File diff suppressed because it is too large
Load diff
30
src/server/phone_video_android.js
Normal file
30
src/server/phone_video_android.js
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
const fs = require('fs');
|
||||
const net = require('net');
|
||||
const path = require('path');
|
||||
const { jpegSplitter } = require('./phone_module');
|
||||
|
||||
const MARKER = path.join(__dirname, '..', '..', '.audio_bridge');
|
||||
|
||||
const bridgePort = () => {
|
||||
try {
|
||||
const port = Number(String(fs.readFileSync(MARKER, 'utf8')).trim());
|
||||
return Number.isFinite(port) && port > 0 ? port : 0;
|
||||
} catch (_) {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = () => {
|
||||
const port = bridgePort();
|
||||
if (!port) return null;
|
||||
return {
|
||||
capture(onJpeg) {
|
||||
const sock = net.connect(port, '127.0.0.1');
|
||||
sock.on('error', () => {});
|
||||
sock.setNoDelay(true);
|
||||
sock.write('V');
|
||||
sock.on('data', jpegSplitter(onJpeg));
|
||||
return { stop() { try { sock.destroy(); } catch (_) {} } };
|
||||
}
|
||||
};
|
||||
};
|
||||
45
src/server/snapshot_plugin.js
Normal file
45
src/server/snapshot_plugin.js
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const pull = require('./node_modules/pull-stream');
|
||||
const defer = require('./node_modules/pull-defer');
|
||||
const toPull = require('./node_modules/stream-to-pull-stream');
|
||||
|
||||
module.exports = {
|
||||
name: 'snapshot',
|
||||
version: '1.0.0',
|
||||
manifest: { info: 'async', get: 'source' },
|
||||
permissions: { anonymous: { allow: ['info', 'get'] } },
|
||||
init(server, config) {
|
||||
const file = (opts) => path.join(config.path, 'oasis', 'content', opts && opts.tier === 'recent' ? 'snapshot-recent.oasissn' : 'snapshot.oasissn');
|
||||
const stat = (opts) => {
|
||||
try { const st = fs.statSync(file(opts)); return { tier: opts && opts.tier === 'recent' ? 'recent' : 'full', bytes: st.size, createdAt: new Date(st.mtimeMs).toISOString() }; } catch (_) { return null; }
|
||||
};
|
||||
const allowed = (rpc, cb) => {
|
||||
const id = rpc && rpc.id;
|
||||
if (!id) return cb(null, false);
|
||||
if (id === server.id) return cb(null, true);
|
||||
if (!server.friends || typeof server.friends.isFollowing !== 'function') return cb(null, false);
|
||||
server.friends.isFollowing({ source: server.id, dest: id }, (err, yes) => cb(null, !err && !!yes));
|
||||
};
|
||||
return {
|
||||
info(opts, cb) {
|
||||
if (typeof opts === 'function') { cb = opts; opts = {}; }
|
||||
allowed(this, (_, ok) => {
|
||||
if (!ok) return cb(new Error('snapshot: not allowed'));
|
||||
const s = stat(opts);
|
||||
if (!s) return cb(new Error('snapshot: not available'));
|
||||
cb(null, s);
|
||||
});
|
||||
},
|
||||
get(opts) {
|
||||
const source = defer.source();
|
||||
allowed(this, (_, ok) => {
|
||||
if (!ok) return source.resolve(pull.error(new Error('snapshot: not allowed')));
|
||||
if (!stat(opts)) return source.resolve(pull.error(new Error('snapshot: not available')));
|
||||
source.resolve(toPull.source(fs.createReadStream(file(opts))));
|
||||
});
|
||||
return source;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
@ -3,8 +3,7 @@ const path = require('path');
|
|||
const Config = require('ssb-config/inject');
|
||||
const minimist = require('minimist');
|
||||
|
||||
const configPath = path.resolve(__dirname, '../configs', 'server-config.json');
|
||||
const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
||||
const configData = require('../configs/config-manager').readServerConfig();
|
||||
|
||||
const argv = process.argv.slice(2);
|
||||
const i = argv.indexOf('--');
|
||||
|
|
@ -43,7 +42,15 @@ if (debug) {
|
|||
|
||||
const megabyte = Math.pow(2, 20);
|
||||
config.blobs = config.blobs || {};
|
||||
config.blobs.max = 50 * megabyte;
|
||||
config.blobs.max = 75 * megabyte;
|
||||
|
||||
config.db2 = { automigrate: false, dangerouslyKillFlumeWhenMigrated: false, ...(config.db2 || {}) };
|
||||
|
||||
const lanOn = (() => { try { return require('../configs/config-manager').getConfig().lanBroadcasting !== false; } catch (_) { return true; } })();
|
||||
const bindHost = config.pub || lanOn ? '0.0.0.0' : '127.0.0.1';
|
||||
for (const entry of (config.connections && config.connections.incoming && config.connections.incoming.net) || []) {
|
||||
if (entry && typeof entry === 'object' && !entry.host) entry.host = bindHost;
|
||||
}
|
||||
|
||||
config.account = account;
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,23 @@ const pkg = require('./package.json');
|
|||
const config = require('./ssb_config');
|
||||
const updater = require('../backend/updater.js');
|
||||
|
||||
const formatMB = (bytes, decimals = 0) => `${(bytes / (1024 * 1024)).toFixed(decimals)} MB`;
|
||||
|
||||
const blobUsageOnDisk = (ssbPath) => {
|
||||
const out = { bytes: 0, files: 0 };
|
||||
const walk = (dir) => {
|
||||
let entries = [];
|
||||
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch (_) { return; }
|
||||
for (const e of entries) {
|
||||
const full = path.join(dir, e.name);
|
||||
if (e.isDirectory()) walk(full);
|
||||
else if (e.isFile()) { try { out.bytes += fs.statSync(full).size; out.files += 1; } catch (_) {} }
|
||||
}
|
||||
};
|
||||
if (ssbPath) walk(path.join(ssbPath, 'blobs', 'sha256'));
|
||||
return out;
|
||||
};
|
||||
|
||||
let printed = false;
|
||||
let checkedForUpdate = false;
|
||||
let pendingClearnetModules = null;
|
||||
|
|
@ -90,40 +107,90 @@ async function printMetadata(mode, modeColor = colors.cyan, httpPort = 3000, htt
|
|||
const httpUrl = hasHttp ? `http://${httpHost}:${httpPort}` : '';
|
||||
const oscLink = hasHttp ? `\x1b]8;;${httpUrl}\x07${httpUrl}\x1b]8;;\x07` : '';
|
||||
const ssbPort = config.connections?.incoming?.net?.[0]?.port || config.port || 8008;
|
||||
const localDiscovery = config.local === true;
|
||||
const localDiscovery = (() => { try { return require('../configs/config-manager.js').getConfig().lanBroadcasting !== false; } catch (_) { return false; } })();
|
||||
const hops = config.conn?.hops ?? config.friends?.hops ?? 2;
|
||||
|
||||
console.log("=========================");
|
||||
console.log(`Running mode: ${modeColor}${mode}${colors.reset}`);
|
||||
console.log("=========================");
|
||||
const walletId = (() => {
|
||||
try {
|
||||
const w = (require('../configs/config-manager.js').getConfig() || {}).wallet || {};
|
||||
if (!(String(w.url || '').trim() && String(w.user || '').trim() && String(w.pass || '').trim())) return '';
|
||||
const file = process.env.OASIS_BANKING_DIR ? path.join(process.env.OASIS_BANKING_DIR, 'wallet-addresses.json') : require('../configs/state-manager').statePath('wallet-addresses.json');
|
||||
const map = JSON.parse(fs.readFileSync(file, 'utf8'));
|
||||
const v = map[`@${publicKey}`];
|
||||
const addr = typeof v === 'string' ? v : (v && v.address) || '';
|
||||
return /^E[1-9A-HJ-NP-Za-km-z]{32,34}$/.test(addr) ? addr : '';
|
||||
} catch (_) { return ''; }
|
||||
})();
|
||||
console.log(`- OASIS ID: [ ${colors.orange}@${publicKey}${colors.reset} ]`);
|
||||
console.log(`- Package: ${colors.blue}${name} ${colors.yellow}[Version: ${version}]${colors.reset}`);
|
||||
if (hasHttp) console.log(`- URL: ${colors.cyan}${oscLink}${colors.reset}`);
|
||||
console.log(`- Oasis ID: [ ${colors.orange}@${publicKey}${colors.reset} ]`);
|
||||
if (hasHttp) console.log(`- GUI: ${colors.cyan}${oscLink}${colors.reset}`);
|
||||
const phoneEnabled = (() => {
|
||||
try { return ((require('../configs/config-manager.js').getConfig() || {}).modules || {}).phoneMod !== 'off'; } catch (_) { return false; }
|
||||
})();
|
||||
const phoneNumber = phoneEnabled ? (() => { try { return require('../models/phone_number').phoneNumberOf(`@${publicKey}`); } catch (_) { return null; } })() : null;
|
||||
console.log(`- VoIP ID: ${phoneEnabled ? (phoneNumber ? `[ ${colors.orange}${phoneNumber}${colors.reset} ]` : 'enabled') : 'disabled'}`);
|
||||
console.log(walletId ? `- ECOin ID: [ ${colors.orange}${walletId}${colors.reset} ]` : `- ECOin ID: disabled`);
|
||||
console.log("- Logging Level:", logLevel);
|
||||
console.log("- Engine: db2-legacy");
|
||||
const ifaces = os.networkInterfaces();
|
||||
const isOnline = Object.values(ifaces).some(list =>
|
||||
list && list.some(i => !i.internal && i.family === 'IPv4')
|
||||
);
|
||||
console.log(`- Protocol (port): ${ssbPort}`);
|
||||
console.log(`- Mode: ${isOnline ? 'online' : 'offline'}`);
|
||||
const networkPaused = (() => { try { return require('../configs/config-manager.js').getConfig().networkPaused === true || process.env.OASIS_NETWORK_PAUSED === '1'; } catch (_) { return false; } })();
|
||||
console.log(`- Mode: ${networkPaused ? 'paused' : (isOnline ? 'online' : 'offline')}`);
|
||||
console.log(`- Replication (hops): ${hops}`);
|
||||
const oasisCfg = (() => { try { return require('../configs/config-manager.js').getConfig() || {}; } catch (_) { return {}; } })();
|
||||
console.log(`- Blockchain backlog: ${Number(oasisCfg.ssbLogStream && oasisCfg.ssbLogStream.limit) || 1000} blocks`);
|
||||
console.log(`- Blob size limit: ${formatMB(Number(config.blobs && config.blobs.max) || 0)}`);
|
||||
const cacheMb = Number(oasisCfg.blobCache && (isPublic ? oasisCfg.blobCache.pubMaxMB : oasisCfg.blobCache.maxMB)) || 0;
|
||||
const cacheUsage = blobUsageOnDisk(config.path);
|
||||
console.log(`- Media cache limit: ${cacheMb > 0 ? formatMB(cacheMb * 1024 * 1024) : 'Unlimited'}`);
|
||||
console.log(`- Current usage: ${formatMB(cacheUsage.bytes, 1)} · ${cacheUsage.files} files`);
|
||||
console.log(`- LAN Broadcasting (UDP): ${localDiscovery ? 'enabled' : 'disabled'}`);
|
||||
const clearnetModules = await waitForClearnet();
|
||||
const clearnetStatus = (clearnetModules && clearnetModules.length > 0) ? clearnetModules.join(', ') : 'disabled';
|
||||
let fediverseConnected = false;
|
||||
try {
|
||||
const acc = JSON.parse(fs.readFileSync(path.join(__dirname, '../configs/fediverse-accounts.json'), 'utf8'));
|
||||
fediverseConnected = !!(acc && acc.mastodon);
|
||||
} catch (_) {}
|
||||
const multiverse = (() => {
|
||||
if ((oasisCfg.modules || {}).fediverseMod === 'off') return [];
|
||||
try {
|
||||
const acc = JSON.parse(fs.readFileSync(require('../configs/state-manager').statePath('fediverse-accounts.json'), 'utf8')) || {};
|
||||
return [
|
||||
acc.mastodon && acc.mastodon.token ? 'Mastodon' : null,
|
||||
acc.peertube && acc.peertube.token ? 'PeerTube' : null,
|
||||
acc.telegram && acc.telegram.session ? 'Telegram' : null
|
||||
].filter(Boolean);
|
||||
} catch (_) { return []; }
|
||||
})();
|
||||
console.log(`- Internet Broadcasting:`);
|
||||
console.log(` - Clearnet: ${clearnetStatus}`);
|
||||
console.log(` - Fediverse: ${fediverseConnected ? 'enabled' : 'disabled'}`);
|
||||
console.log(` - Multiverse: ${multiverse.length ? multiverse.join(', ') : 'disabled'}`);
|
||||
if (hasHttp) {
|
||||
const en = (() => { try { return require('../client/assets/translations/i18n').en || {}; } catch (_) { return {}; } })();
|
||||
const uxNames = { blocks: en.uxModeMenus, ainav: en.uxModeAINav, chats: en.uxModeChats, feed: en.uxModeFeed, phone: en.phoneTitle };
|
||||
const wishNames = { whole: en.settingsWishWhole, mutuals: en.settingsWishMutuals, 'only-lan': en.settingsWishOnlyLan, local: en.settingsWishLocal };
|
||||
const workflow = (() => {
|
||||
try {
|
||||
const wf = require('../models/workflows_model');
|
||||
const key = wf.currentWorkflow(oasisCfg);
|
||||
if (key) return en[`workflow_${key}`] || key;
|
||||
const mods = oasisCfg.modules || {};
|
||||
return wf.ALL_MODULES.every(m => mods[`${m}Mod`] === 'on') ? (en.welcomeWorkflow_full || 'Default') : 'custom';
|
||||
} catch (_) { return 'custom'; }
|
||||
})();
|
||||
const ux = (oasisCfg.ux && oasisCfg.ux.current) || 'blocks';
|
||||
const wish = oasisCfg.wish || 'whole';
|
||||
console.log(`- Workflow: ${workflow}`);
|
||||
console.log(`- UX mode: ${uxNames[ux] || ux}`);
|
||||
console.log(`- Wish: ${wishNames[wish] || wish}`);
|
||||
}
|
||||
console.log("");
|
||||
console.log("=========================");
|
||||
console.log("Modules loaded: [", modules.length, "]");
|
||||
console.log("=========================");
|
||||
|
||||
// Check for updates
|
||||
await checkForUpdate();
|
||||
console.log("=========================");
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue