mesh: replace notify webhook with in-process opencode notifier; drop forwarder service
This commit is contained in:
+15
-47
@@ -9,6 +9,7 @@ import * as net from 'node:net';
|
||||
// type-only import: node:sqlite is only required at runtime by whoever opens a database (mesh.module.ts / tests)
|
||||
import type { DatabaseSync } from 'node:sqlite';
|
||||
import type * as Proto from '@meshtastic/protobufs';
|
||||
import { OpencodeService } from '../opencode/opencode.service.js';
|
||||
import {
|
||||
BROADCAST_ADDR,
|
||||
PORT_NODEINFO_APP,
|
||||
@@ -132,8 +133,6 @@ function jsonSafe<T>(value: T): T {
|
||||
|
||||
// key material must never leave the process — replaced with a marker so callers can see the field exists
|
||||
const REDACTED = 'REDACTED';
|
||||
// ponytail: hardcoded notify-webhook backoff [2s, 10s, 30s] (~45 s window) — no env knob; tune here if outages run longer
|
||||
const NOTIFY_RETRY_DELAYS_MS = [2_000, 10_000, 30_000];
|
||||
/** Config sections that carry secrets: security (PKI keys) and network (wifi psk). */
|
||||
function redactSection(
|
||||
name: string,
|
||||
@@ -161,8 +160,8 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
|
||||
process.env.MESH_SEND_CHANNEL ?? '3',
|
||||
10,
|
||||
);
|
||||
// outbound notification webhook: fire-and-forget POST when an rx text lands on the notify channel; empty URL = disabled
|
||||
private readonly notifyUrl = process.env.MESH_NOTIFY_URL ?? '';
|
||||
// outbound notification: fire-and-forget push to the opencode notifier when an rx text lands on the notify channel;
|
||||
// disabled state (missing credentials) lives in OpencodeService itself
|
||||
private readonly notifyChannel = parseInt(
|
||||
process.env.MESH_NOTIFY_CHANNEL ?? '3',
|
||||
10,
|
||||
@@ -172,10 +171,13 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
/** SQLite database backing the message history; null = persistence disabled (tests construct without one). */
|
||||
private db: DatabaseSync | null = null;
|
||||
/** In-process opencode notifier for rx texts on the notify channel; null in tests that don't wire one. */
|
||||
private readonly opencode: OpencodeService | null;
|
||||
private pollTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
constructor(db?: DatabaseSync) {
|
||||
constructor(db?: DatabaseSync, opencode?: OpencodeService) {
|
||||
this.db = db ?? null;
|
||||
this.opencode = opencode ?? null;
|
||||
}
|
||||
|
||||
private socket: net.Socket | null = null;
|
||||
@@ -430,54 +432,20 @@ export class MeshService implements OnModuleInit, OnModuleDestroy {
|
||||
at: Date.now(),
|
||||
};
|
||||
this.pushHistory(rec);
|
||||
// injected records ride channel 0 — the webhook fires only when MESH_NOTIFY_CHANNEL is set to match
|
||||
// injected records ride channel 0 — the notifier fires only when MESH_NOTIFY_CHANNEL is set to match
|
||||
this.notifyRx(0, text, from);
|
||||
return { id: rec.id };
|
||||
}
|
||||
|
||||
/** Fire-and-forget notify-webhook POST for an rx text on `channel` (no-op unless URL is set and channels match). */
|
||||
/** Fire-and-forget opencode notifier push for an rx text on `channel` (no-op unless channels match and a notifier is wired). */
|
||||
private notifyRx(channel: number, text: string, from?: number): void {
|
||||
if (this.notifyUrl && channel === this.notifyChannel) {
|
||||
// self-echo filter: our own node must never trigger a notification of itself
|
||||
if (from !== undefined && from === this.myNodeNum) {
|
||||
this.logger.debug(`notify skipped: rx from our own node ${from}`);
|
||||
return;
|
||||
}
|
||||
const body: Record<string, unknown> = { text };
|
||||
if (from !== undefined) body.from = from;
|
||||
void this.postNotify(body);
|
||||
if (!this.opencode || channel !== this.notifyChannel) return;
|
||||
// self-echo filter: our own node must never trigger a notification of itself
|
||||
if (from !== undefined && from === this.myNodeNum) {
|
||||
this.logger.debug(`notify skipped: rx from our own node ${from}`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** POST the webhook with retries: up to NOTIFY_RETRY_DELAYS_MS.length + 1 attempts total. */
|
||||
private async postNotify(body: Record<string, unknown>): Promise<void> {
|
||||
const maxAttempts = NOTIFY_RETRY_DELAYS_MS.length + 1;
|
||||
for (let attempt = 1; ; attempt++) {
|
||||
try {
|
||||
const res = await fetch(this.notifyUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (res && !res.ok) throw new Error('HTTP ' + res.status); // HTTP errors retry like network failures
|
||||
return; // success on any attempt — same as today's single POST
|
||||
} catch (err: unknown) {
|
||||
const msg = (err as Error).message;
|
||||
this.logger.warn(
|
||||
`notify webhook failed (attempt ${attempt}/${maxAttempts}): ${msg}`,
|
||||
);
|
||||
if (attempt === maxAttempts) {
|
||||
this.logger.warn(`notify dropped after ${attempt} attempts: ${msg}`);
|
||||
return;
|
||||
}
|
||||
await this.sleep(NOTIFY_RETRY_DELAYS_MS[attempt - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Injectable so specs can skip the backoff delays. */
|
||||
private sleep(ms: number): Promise<void> {
|
||||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||||
void this.opencode.notify(text, from);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user