minor refactoring

This commit is contained in:
Egor 2026-01-22 12:06:06 -08:00
parent cb5a7e870a
commit 606c4c6f23
7 changed files with 138 additions and 139 deletions

View file

@ -17,7 +17,7 @@ import { PUBLIC_DIR } from '@/constants/paths';
import { cleanupDaemon, initSignal } from '@/modules/signal';
import admin from '@/routes/admin';
import ntfy from '@/routes/ntfy';
import unifiedpush from '@/routes/unifiedpush';
import unifiedpush from '@/routes/unified-push';
import { isLocalIP } from '@/utils/auth';
import { logError, logInfo, logVerbose, logWarn } from '@/utils/log';
@ -33,7 +33,7 @@ if (!API_KEY) {
if (PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD) {
try {
const { startProtonMonitor } = await import('./modules/protonmail');
const { startProtonMonitor } = await import('./modules/proton-mail');
await startProtonMonitor();
} catch (err) {
logError('Failed to start Proton Mail monitor:', err);

View file

@ -37,8 +37,6 @@ export async function startProtonMonitor() {
password: PROTON_IMAP_PASSWORD,
host: PROTON_BRIDGE_HOST,
port: PROTON_BRIDGE_PORT,
tls: false,
tlsOptions: { rejectUnauthorized: false },
keepalive: true,
});
@ -87,9 +85,11 @@ export async function startProtonMonitor() {
fetch.on('message', (msg) => {
msg.on('body', (stream) => {
let buffer = '';
stream.on('data', (chunk) => {
buffer += chunk.toString('utf8');
});
stream.once('end', () => {
const header = Imap.parseHeader(buffer);
const rawFrom = header.from?.[0] || 'Unknown sender';
@ -98,12 +98,15 @@ export async function startProtonMonitor() {
if (dateStr) {
const messageDate = new Date(dateStr).getTime();
if (messageDate < monitorStartTime) {
const formattedDate = new Date(dateStr).toLocaleString('en-US', {
dateStyle: 'medium',
timeStyle: 'short',
});
logVerbose(`Skipping old email: ${subject} (${formattedDate})`);
return;
}
}

View file

@ -94,7 +94,7 @@ export async function finishLink() {
return result;
}
export async function unlinkDevice() {
async function unlinkDevice() {
account = null;
currentLinkUri = null;

View file

@ -1,6 +1,6 @@
import { Database } from 'bun:sqlite';
import { SUP_DB } from '@/constants/paths';
import { createGroup } from './signal';
import { createGroup } from '@/modules/signal';
interface EndpointMapping {
endpoint: string;
@ -18,18 +18,18 @@ db.run(`
)
`);
export const register = (endpoint: string, groupId: string, appName: string) => {
export const register = (endpoint: string, groupId: string, appName: string) =>
db.run('INSERT OR REPLACE INTO mappings (endpoint, groupId, appName) VALUES (?, ?, ?)', [
endpoint,
groupId,
appName,
]);
};
export const getGroupId = (endpoint: string) => {
const row = db.query('SELECT groupId FROM mappings WHERE endpoint = ?').get(endpoint) as
| { groupId: string }
| undefined;
return row?.groupId;
};
@ -37,15 +37,15 @@ export const getAppName = (endpoint: string) => {
const row = db.query('SELECT appName FROM mappings WHERE endpoint = ?').get(endpoint) as
| { appName: string }
| undefined;
return row?.appName;
};
export const getAllMappings = () =>
db.query('SELECT endpoint, groupId, appName FROM mappings').all() as EndpointMapping[];
export const remove = (endpoint: string) => {
export const remove = (endpoint: string) =>
db.run('DELETE FROM mappings WHERE endpoint = ?', [endpoint]);
};
export const getOrCreateGroup = async (key: string, name: string) => {
const existingGroupId = getGroupId(key);
@ -53,5 +53,6 @@ export const getOrCreateGroup = async (key: string, name: string) => {
const groupId = await createGroup(name);
register(key, groupId, name);
return groupId;
};

View file

@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { basicAuth } from 'hono/basic-auth';
import { DEVICE_NAME, PROTON_IMAP_PASSWORD, PROTON_IMAP_USERNAME } from '@/constants/config';
import { isImapConnected } from '@/modules/protonmail';
import { isImapConnected } from '@/modules/proton-mail';
import {
checkSignalCli,
finishLink,
@ -19,133 +19,6 @@ let qrCacheTime = 0;
let generatingPromise: Promise<string> | null = null;
const QR_CACHE_TTL = 10 * 60 * 1000;
export const handleHealthFragment = async () => {
const signalOk = await checkSignalCli();
const linked = signalOk && (await hasValidAccount());
const imap = isImapConnected();
const hasProtonConfig = PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD;
const accountNumber = getAccount();
const html = `
<div class="status">
<div class="status-item ${signalOk ? 'status-ok' : 'status-error'}">
Signal Network: ${signalOk ? 'Connected' : 'Disconnected'}
</div>
<div class="status-item ${linked ? 'status-ok' : 'status-error'}">
Account: ${linked ? 'Linked' : 'Unlinked'}
${linked && accountNumber ? `<span class="tooltip">${formatPhoneNumber(accountNumber)}</span>` : ''}
</div>
${
hasProtonConfig
? `<div class="status-item ${imap ? 'status-ok' : 'status-error'}">
Proton Mail: ${imap ? 'Connected' : 'Disconnected'}
${imap ? `<span class="tooltip">${PROTON_IMAP_USERNAME}</span>` : ''}
</div>`
: ''
}
</div>
<div id="signal-info" hx-swap-oob="true">
${await handleSignalInfoFragment()}
</div>
`;
return { html, linked };
};
export const handleSignalInfoFragment = async () => {
if (await hasValidAccount()) {
cachedQR = null;
return `<details class="unlink-details">
<summary class="unlink-summary">Unlink and remove device</summary>
<div class="unlink-instructions">
<ol>
<li>Open Signal app <strong>Settings Linked Devices</strong></li>
<li>Find <strong>"${DEVICE_NAME}"</strong> and tap it</li>
<li>Tap <strong>"Unlink Device"</strong></li>
</ol>
</div>
</details>`;
}
return handleQRSection();
};
export const handleEndpointsFragment = async () => {
const endpoints = getAllMappings();
if (endpoints.length === 0) {
return '<p>No endpoints registered</p>';
}
return `
<ul class="endpoint-list">
${endpoints
.map(
(e: { appName: string; endpoint: string }) => `
<li class="endpoint-item">
<div class="endpoint-name">
<strong>${e.appName}</strong>
</div>
<button
class="btn-delete"
hx-delete="/action/delete-endpoint/${encodeURIComponent(e.endpoint)}"
hx-target="#endpoints-list"
hx-swap="innerHTML"
>Delete</button>
</li>
`,
)
.join('')}
</ul>
`;
};
export const handleQRSection = async () => {
if (await hasValidAccount()) {
return '<p>Account already linked</p>';
}
const now = Date.now();
if ((!cachedQR || now - qrCacheTime > QR_CACHE_TTL) && !generatingPromise) {
generatingPromise = (async () => {
const qr = await generateLinkQR();
cachedQR = qr;
qrCacheTime = Date.now();
finishLink()
.then(async () => {
await initSignal();
})
.catch(() => {})
.finally(() => {
generatingPromise = null;
cachedQR = null;
qrCacheTime = 0;
});
return qr;
})();
}
if (generatingPromise && !cachedQR) {
await generatingPromise;
}
return `
<p>Scan this QR code with your Signal app:</p>
<p class="qr-instructions"><strong>Settings Linked Devices Link New Device</strong></p>
<div class="qr-container">
<img src="${cachedQR}" class="qr-image" alt="QR Code" />
</div>
`;
};
export const handleLinkStatusCheck = async () => {
const linked = await hasValidAccount();
return { linked };
};
const admin = new Hono();
admin.use(
@ -198,4 +71,126 @@ admin.delete('/action/delete-endpoint/:endpoint', async (c) => {
return c.html(await handleEndpointsFragment());
});
const handleHealthFragment = async () => {
const signalOk = await checkSignalCli();
const linked = signalOk && (await hasValidAccount());
const imap = isImapConnected();
const hasProtonConfig = PROTON_IMAP_USERNAME && PROTON_IMAP_PASSWORD;
const accountNumber = getAccount();
const html = `
<div class="status">
<div class="status-item ${signalOk ? 'status-ok' : 'status-error'}">
Signal Network: ${signalOk ? 'Connected' : 'Disconnected'}
</div>
<div class="status-item ${linked ? 'status-ok' : 'status-error'}">
Account: ${linked ? 'Linked' : 'Unlinked'}
${linked && accountNumber ? `<span class="tooltip">${formatPhoneNumber(accountNumber)}</span>` : ''}
</div>
${
hasProtonConfig
? `<div class="status-item ${imap ? 'status-ok' : 'status-error'}">
Proton Mail: ${imap ? 'Connected' : 'Disconnected'}
${imap ? `<span class="tooltip">${PROTON_IMAP_USERNAME}</span>` : ''}
</div>`
: ''
}
</div>
<div id="signal-info" hx-swap-oob="true">
${await handleSignalInfoFragment()}
</div>
`;
return { html, linked };
};
const handleSignalInfoFragment = async () => {
if (await hasValidAccount()) {
cachedQR = null;
return `<details class="unlink-details">
<summary class="unlink-summary">Unlink and remove device</summary>
<div class="unlink-instructions">
<ol>
<li>Open Signal app <strong>Settings Linked Devices</strong></li>
<li>Find <strong>"${DEVICE_NAME}"</strong> and tap it</li>
<li>Tap <strong>"Unlink Device"</strong></li>
</ol>
</div>
</details>`;
}
return handleQRSection();
};
const handleEndpointsFragment = async () => {
const endpoints = getAllMappings();
if (endpoints.length === 0) {
return '<p>No endpoints registered</p>';
}
return `
<ul class="endpoint-list">
${endpoints
.map(
(e: { appName: string; endpoint: string }) => `
<li class="endpoint-item">
<div class="endpoint-name">
<strong>${e.appName}</strong>
</div>
<button
class="btn-delete"
hx-delete="/action/delete-endpoint/${encodeURIComponent(e.endpoint)}"
hx-target="#endpoints-list"
hx-swap="innerHTML"
>Delete</button>
</li>
`,
)
.join('')}
</ul>
`;
};
const handleQRSection = async () => {
if (await hasValidAccount()) {
return '<p>Account already linked</p>';
}
const now = Date.now();
if ((!cachedQR || now - qrCacheTime > QR_CACHE_TTL) && !generatingPromise) {
generatingPromise = (async () => {
const qr = await generateLinkQR();
cachedQR = qr;
qrCacheTime = Date.now();
finishLink()
.then(async () => {
await initSignal();
})
.catch(() => {})
.finally(() => {
generatingPromise = null;
cachedQR = null;
qrCacheTime = 0;
});
return qr;
})();
}
if (generatingPromise && !cachedQR) {
await generatingPromise;
}
return `
<p>Scan this QR code with your Signal app:</p>
<p class="qr-instructions"><strong>Settings Linked Devices Link New Device</strong></p>
<div class="qr-container">
<img src="${cachedQR}" class="qr-image" alt="QR Code" />
</div>
`;
};
export default admin;

View file

@ -1,7 +1,7 @@
import { Hono } from 'hono';
import { sendGroupMessage } from '@/modules/signal';
import { getGroupId, getOrCreateGroup, remove } from '@/modules/store';
import { formatAsSignalMessage, parseUnifiedPushRequest } from '@/modules/unifiedpush';
import { formatAsSignalMessage, parseUnifiedPushRequest } from '@/modules/unified-push';
const unifiedpush = new Hono();