import { rm, unlink } from 'node:fs/promises'; import { DEVICE_NAME, VERBOSE } from '../constants/config'; import { SIGNAL_CLI, SIGNAL_CLI_DATA, SIGNAL_CLI_SOCKET } from '../constants/paths'; import type { ListAccountsResult, StartLinkResult, UpdateGroupResult } from '../types'; import { logError, logInfo, logSuccess, logVerbose, logWarn } from '../utils/log'; import { call } from '../utils/rpc'; logVerbose(`Running signal-cli from ${SIGNAL_CLI}`); let account: string | null = null; let currentLinkUri: string | null = null; export const hasLinkUri = () => currentLinkUri !== null; export async function initSignal({ accountOverride }: { accountOverride?: string }) { if (accountOverride) { account = accountOverride; return true; } const result = (await call('listAccounts', {}, account)) as ListAccountsResult; const [firstAccount] = result; if (firstAccount) { account = firstAccount; return true; } return false; } export async function generateLinkQR() { const result = (await call( 'startLink', { deviceName: DEVICE_NAME, }, account, )) as StartLinkResult; const uri = result.deviceLinkUri; if (!uri) { throw new Error('Failed to generate linking URI'); } currentLinkUri = uri; return `https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(uri)}`; } export async function finishLink() { if (!currentLinkUri) { throw new Error('No link in progress'); } const uri = currentLinkUri; currentLinkUri = null; const result = await call( 'finishLink', { deviceLinkUri: uri, deviceName: DEVICE_NAME, }, account, ); logSuccess('✓ Device linked successfully'); return result; } export async function unlinkDevice() { account = null; currentLinkUri = null; if (await Bun.file(SIGNAL_CLI_DATA).exists()) { logWarn('⚠ Unlinking device and removing account data...'); try { await rm(SIGNAL_CLI_DATA, { recursive: true, force: true }); } catch (error) { logWarn('❌ Failed to remove account data directory:', error); } } } export async function createGroup(name: string, members: string[] = []) { const result = (await call( 'updateGroup', { name, member: members, }, account, )) as UpdateGroupResult; if (!result?.groupId) { throw new Error('Failed to create group'); } return result.groupId; } export async function sendGroupMessage(groupId: string, message: string) { await call( 'send', { groupId, message, }, account, ); } export async function checkSignalCli() { try { await call('listAccounts', {}, account); return true; } catch { return false; } } export async function hasValidAccount() { try { const result = (await call('listAccounts', {}, account)) as ListAccountsResult; return result.length > 0; } catch { return false; } } export async function startDaemon() { let authError = false; let cleaned = false; if (await Bun.file(SIGNAL_CLI_SOCKET).exists()) { try { await unlink(SIGNAL_CLI_SOCKET); logVerbose('Removed stale socket file'); } catch (error) { logError('Failed to remove stale socket file:', error); } } const proc = Bun.spawn([SIGNAL_CLI, 'daemon', '--socket', SIGNAL_CLI_SOCKET], { stdout: 'pipe', stderr: 'pipe', }); (async () => { for await (const chunk of proc.stderr) { const text = new TextDecoder().decode(chunk); const trimmed = text.trim(); if (!trimmed) continue; if (trimmed.includes('Authorization failed') || trimmed.includes('AccountCheckException')) { authError = true; } if (trimmed.includes('ConcurrentModificationException')) continue; if (VERBOSE) { if (trimmed.includes('ERROR')) { logError('[signal-cli]', trimmed); } else if (trimmed.includes('WARN')) { logWarn('[signal-cli]', trimmed); } else { logInfo('[signal-cli]', trimmed); } continue; } if (trimmed.includes('WARN')) { logWarn('[signal-cli]', trimmed); } else if (trimmed.includes('ERROR') || !trimmed.includes('INFO')) { logError('[signal-cli]', trimmed); } } })(); await Bun.sleep(3000); try { const socket = await Bun.connect({ unix: SIGNAL_CLI_SOCKET, socket: { data() {}, }, }); socket.end(); logSuccess('✓ signal-cli daemon started'); return proc; } catch (error) { if (authError && !cleaned) { logWarn('⚠ Detected stale account data, cleaning up and retrying...'); proc.kill(); await unlinkDevice(); cleaned = true; return startDaemon(); } logError('Failed to connect to signal-cli socket:', error); if (proc.exitCode !== null) { logError('signal-cli process exited with code:', proc.exitCode); } throw new Error('Failed to start signal-cli daemon'); } }