Signal chats in the app (desktop only) with a chat button on every screen
build-container / build (push) Successful in 6m44s

- signal.js: talks to signal-cli-rest-api (a linked device of your Signal account); keeps conversations in /data/signal; live updates; send/receive text, files, replies, reactions, groups, receipts
- Signal tab and a floating chat panel on every screen, shown only on desktop-class windows; new Signal permission per person
- compose: signal bridge container sharing the hub's network
- tools/signal-test.js (fake bridge), README

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 5.5 committed 2026-10-21 10:18:02 -04:00
1 parent 5157e42bc4
commit d0070aa0bb
10 files changed
+746 -11

No files matched your search

+5 -1
View File
@@ -35,6 +35,7 @@ const settings = require('./settings');
const search = require('./search');
const users = require('./users');
const admin = require('./admin');
const signal = require('./signal'); // Signal messages, through signal-cli-rest-api (SIGNAL_API_URL)
const vault = require('./vault')('nehub-plex');
const VERSE_URL = process.env.VERSE_URL || 'https://versebyversewithnate.us/admin';
const PODCAST_URL = process.env.PODCAST_URL || 'https://podcast.necloud.us/';
@@ -408,6 +409,7 @@ function storageInfo() {
startedAt: STARTED };
}
const STARTED = Date.now();
signal.init({ dataDir: path.resolve(process.env.DATA_DIR || path.join(__dirname, 'data')), apiUrl: process.env.SIGNAL_API_URL || '' });
{ const i = storageInfo(); console.log(`[data] ${i.dir}: ${i.passkeys} passkeys, ${i.calendarLinks} calendar links, ${i.events} events; mounted: ${i.mounted === null ? 'unknown' : i.mounted ? (i.source.kind + ' ' + i.source.name) : 'NO (data is lost on redeploy)'}; writable: ${i.writable}; COOKIE_SECRET from env: ${i.secretFromEnv}`);
if (i.source && i.source.kind === 'anonymous') console.warn('[data] WARNING: /data is an anonymous Docker volume, not your host folder. It is deleted when the container is recreated. Check that your stack mounts the host folder at /data.');
if (i.mounted === false) console.warn('[data] WARNING: the data folder is not a mounted volume. Passkeys, calendars and sessions will be lost whenever the container is recreated. Mount a volume or host folder at /data.'); }
@@ -442,6 +444,7 @@ const ACCESS = [
[/^\/api\/oo\/config$/, ['office', 'files']],
[/^\/(api\/plex\/|plex\/)/, ['music']],
[/^\/api\/audiobooks\//, ['audio']],
[/^\/api\/signal\//, ['signal']],
[/^\/(webmail(\/|$)|api\/webmail\/)/, ['mail']],
[/^\/api\/cal\//, ['calendar']],
[/^\/api\/tasks/, ['tasks']],
@@ -477,7 +480,7 @@ async function handleRequest(req, res) {
const c = t => users.can(user, t);
res.writeHead(200, { 'content-type': 'application/json' });
return res.end(JSON.stringify({ seafile: c('files') || c('office') ? SEAFILE_URL : '', onlyoffice: c('office') ? ONLYOFFICE_URL : '', audiobooks: c('audio') ? AUDIOBOOKS_URL : '', verse: c('verse') ? VERSE_URL : '',
podcast: c('podcast') ? PODCAST_URL : '', webmail: c('mail') ? (WEBMAIL_URL || (WEBMAIL_BACKEND_URL ? '/webmail/' : '')) : '', editor: c('office') || c('files'), jwt: !!JWT_SECRET, build: BUILD }));
podcast: c('podcast') ? PODCAST_URL : '', signal: c('signal') && signal.enabled() ? '1' : '', webmail: c('mail') ? (WEBMAIL_URL || (WEBMAIL_BACKEND_URL ? '/webmail/' : '')) : '', editor: c('office') || c('files'), jwt: !!JWT_SECRET, build: BUILD }));
}
if (req.method === 'POST' && req.url === '/api/oo/config') return ooConfig(req, res);
if (pathname === '/api/ddg') return ddgAnswer(req, res);
@@ -491,6 +494,7 @@ async function handleRequest(req, res) {
res.writeHead(302, { location: to || AUDIOBOOKS_URL, 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' }); return res.end();
}
if (pathname === '/api/webmail/login') { res.writeHead(200, { 'content-type': 'application/json', 'cache-control': 'no-store' }); return res.end(JSON.stringify(WEBMAIL_BACKEND_URL && CYPHT_PASSWORD ? { user: CYPHT_USER, pass: CYPHT_PASSWORD } : {})); }
if (pathname.startsWith('/api/signal/')) return signal.enabled() ? signal.handle(req, res) : forbidden(res, 'Signal is not set up on this server');
if (WEBMAIL_BACKEND_URL && isWebmailPath(pathname)) return proxyWebmail(req, res);
if (req.url.startsWith('/api/plex/')) return plexApi(req, res);
if (req.url.startsWith('/plex/') && req.method === 'GET') return plexProxy(req, res);