Optional replyId on send (API + MCP) — protocol-level threaded replies (#31)
CI / build + unit (push) Successful in 33s

This commit was merged in pull request #31.
This commit is contained in:
2026-09-25 13:53:47 +02:00
parent 0909fbc6e2
commit 15d93ec2a4
11 changed files with 137 additions and 15 deletions
+19 -2
View File
@@ -13,6 +13,7 @@ import {
ApiResponse,
ApiTags,
} from '@nestjs/swagger';
import { BROADCAST_ADDR } from './codec.js';
import type { DeviceInfo } from './mesh.service.js';
import { MeshService } from './mesh.service.js';
@@ -38,6 +39,13 @@ export class SendTextDto {
example: '!b',
})
to?: string | number;
@ApiProperty({
required: false,
description:
'Packet id of an earlier message (from GET /api/messages) that this one replies to — the recipient app renders it as a threaded reply/quote. Integer in 1..0xffffffff.',
example: 456789,
})
replyId?: number;
}
// tag description is set via DocumentBuilder.addTag in main.ts (v12 ApiTagOptions has no description)
@@ -70,7 +78,7 @@ export class MeshController {
@ApiOperation({
summary: 'Send a text message over the mesh',
description:
'Queues a TEXT_MESSAGE_APP packet to the resolved target (node number, node name, or broadcast). DMs are unicast and wantAck-tracked via GET /api/messages; broadcasts stay pending because firmware strips want_ack. All sends ride the pinned test channel (index 3, "test" — never the primary channel) regardless of DM or broadcast, since modern firmware PKI handles unicast encryption/channel anyway. Returns the Meshtastic packet id.',
'Queues a TEXT_MESSAGE_APP packet to the resolved target (node number, node name, or broadcast). Optional replyId (integer in 1..0xffffffff) references an earlier message\'s packet id from GET /api/messages — when set, the recipient app renders this message as a threaded reply/quote of that packet. DMs are unicast and wantAck-tracked via GET /api/messages; broadcasts stay pending because firmware strips want_ack. All sends ride the pinned test channel (index 3, "test" — never the primary channel) regardless of DM or broadcast, since modern firmware PKI handles unicast encryption/channel anyway. Returns the Meshtastic packet id.',
})
@ApiResponse({
status: 200,
@@ -83,6 +91,15 @@ export class MeshController {
if (typeof body?.text !== 'string' || body.text.length === 0) {
throw new BadRequestException('text must be a non-empty string');
}
const replyId = body.replyId;
if (
replyId !== undefined &&
(!Number.isInteger(replyId) || replyId < 1 || replyId > BROADCAST_ADDR)
) {
throw new BadRequestException(
'replyId must be an integer in 1..0xffffffff',
);
}
let to: number;
try {
to = await this.mesh.resolveDest(body.to);
@@ -91,7 +108,7 @@ export class MeshController {
err instanceof Error ? err.message : String(err),
);
}
return { id: await this.mesh.sendText(to, body.text) };
return { id: await this.mesh.sendText(to, body.text, replyId) };
}
@Get('channels')