Self-hosted multi-platform broadcast newsroom for activist collectives: publishing pipeline with group approval and DST-proof scheduling, TweetDeck-style column wall with hostile-content-safe rendering, live control room with Auto Director, bridged chat governance with polls and quorum, distributed sandboxed transcode workers, encrypted content-addressed archive, monthly analytics. MIT licensed. See README.md and docs/.
107 lines
5.5 KiB
JavaScript
107 lines
5.5 KiB
JavaScript
'use strict';
|
|
// Transfer observation and canonical-origin routing. PeerTube retains queue selection and retries.
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { AsyncLocalStorage } = require('node:async_hooks');
|
|
const context = new AsyncLocalStorage();
|
|
const state = process.env.PT_STATE_DIR || '/var/lib/pt-worker/state';
|
|
let route;
|
|
try {
|
|
const cfg = JSON.parse(fs.readFileSync(process.env.PT_WORKER_CONFIG || '/etc/pt-worker/worker.json'));
|
|
if (cfg.canonical_origin && cfg.peertube_url) route = { from: new URL(cfg.canonical_origin), to: new URL(cfg.peertube_url) };
|
|
} catch { /* Enrollment and isolated tests may not yet have a config. */ }
|
|
const originals = { 'http:': require('node:http').request, 'https:': require('node:https').request };
|
|
function emit(data) {
|
|
try {
|
|
fs.mkdirSync(state, { recursive: true, mode: 0o700 });
|
|
fs.appendFileSync(path.join(state, 'events.jsonl'), JSON.stringify({ time: Date.now() / 1000, ...data }) + '\n', { mode: 0o600 });
|
|
} catch { /* Observation never changes job behavior. */ }
|
|
}
|
|
const createWriteStream = fs.createWriteStream;
|
|
fs.createWriteStream = function (filename, ...args) {
|
|
const pathname = context.getStore();
|
|
const match = pathname?.match(/\/api\/v1\/runners\/jobs\/([a-f\d-]{36})\/files\//);
|
|
if (match && typeof filename === 'string') {
|
|
try {
|
|
const dir = path.join(state, 'inputs');
|
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
fs.writeFileSync(path.join(dir, path.basename(filename) + '.json'), JSON.stringify({ uuid: match[1] }), { mode: 0o600 });
|
|
} catch { /* Optional correlation only. */ }
|
|
}
|
|
return createWriteStream.call(this, filename, ...args);
|
|
};
|
|
require('node:module').syncBuiltinESMExports();
|
|
for (const name of ['node:http', 'node:https']) {
|
|
const mod = require(name);
|
|
const original = mod.request;
|
|
mod.request = function (...args) {
|
|
let selected = original;
|
|
// Source payloads use the public origin. Route only that origin via Tailscale.
|
|
let options = args[0];
|
|
if (typeof options === 'string' || options instanceof URL) {
|
|
const u = new URL(options);
|
|
options = { protocol: u.protocol, hostname: u.hostname, port: u.port, path: u.pathname + u.search,
|
|
...(typeof args[1] === 'object' ? args[1] : {}) };
|
|
}
|
|
if (route && (options.hostname || options.host) === route.from.hostname &&
|
|
(options.protocol || (name === 'node:https' ? 'https:' : 'http:')) === route.from.protocol &&
|
|
String(options.port || (route.from.protocol === 'https:' ? 443 : 80)) === String(route.from.port || (route.from.protocol === 'https:' ? 443 : 80))) {
|
|
const callback = typeof args[args.length - 1] === 'function' ? args[args.length - 1] : undefined;
|
|
options = { ...options, protocol: route.to.protocol, hostname: route.to.hostname, port: route.to.port,
|
|
headers: { ...(options.headers || {}), host: route.from.host } };
|
|
delete options.host;
|
|
selected = originals[route.to.protocol];
|
|
args = callback ? [options, callback] : [options];
|
|
}
|
|
let req;
|
|
if (typeof args[args.length - 1] === 'function') {
|
|
const callback = args[args.length - 1];
|
|
args[args.length - 1] = function (...values) {
|
|
return context.run((req.path || '').split('?')[0], () => callback.apply(this, values));
|
|
};
|
|
}
|
|
req = selected.apply(this, args);
|
|
// No host, URL, query, headers, body, or tokens are ever recorded.
|
|
const pathname = (req.path || '').split('?')[0];
|
|
const match = pathname.match(/\/api\/v1\/runners\/jobs\/([a-f\d-]{36})\/(accept|success|error|abort)/);
|
|
const download = pathname.includes('/files/');
|
|
const upload = (match && match[2] === 'success') || (req.method === 'PUT');
|
|
const begun = performance.now();
|
|
let sent = 0, received = 0, done = false, requestErrored = false;
|
|
function size(value, encoding) {
|
|
if (Buffer.isBuffer(value) || value instanceof Uint8Array) return value.length;
|
|
if (typeof value === 'string') return Buffer.byteLength(value, typeof encoding === 'string' ? encoding : 'utf8');
|
|
return 0;
|
|
}
|
|
const write = req.write;
|
|
req.write = function (chunk, ...rest) { sent += size(chunk, rest[0]); return write.call(this, chunk, ...rest); };
|
|
const end = req.end;
|
|
req.end = function (chunk, ...rest) { sent += size(chunk, rest[0]); return end.call(this, chunk, ...rest); };
|
|
function finish(ok, code) {
|
|
if (done) return;
|
|
done = true;
|
|
const seconds = Math.max(0.001, (performance.now() - begun) / 1000);
|
|
if (download || upload) emit({ kind: 'transfer', direction: download ? 'download' : 'upload',
|
|
bytes: download ? received : sent, seconds, ok, status_code: code || null });
|
|
if (match) emit({ kind: 'job_' + match[2], uuid: match[1], ok });
|
|
}
|
|
req.on('response', res => {
|
|
function interrupted() {
|
|
finish(false, res.statusCode);
|
|
req.destroy();
|
|
// A ClientRequest may already be marked destroyed when its response aborts;
|
|
// destroy(error) then emits nothing. Forward one error to the upstream promise.
|
|
queueMicrotask(() => {
|
|
if (!requestErrored) req.emit('error', new Error('Worker transfer stream interrupted'));
|
|
});
|
|
}
|
|
res.on('data', chunk => { received += chunk.length; });
|
|
res.once('end', () => finish(res.statusCode >= 200 && res.statusCode < 300, res.statusCode));
|
|
res.once('aborted', interrupted);
|
|
res.once('error', interrupted);
|
|
});
|
|
req.on('error', () => { requestErrored = true; finish(false); });
|
|
return req;
|
|
};
|
|
}
|
|
require('node:module').syncBuiltinESMExports();
|