diff --git a/src/channels/discord.test.ts b/src/channels/discord.test.ts index 048b486..82713e6 100644 --- a/src/channels/discord.test.ts +++ b/src/channels/discord.test.ts @@ -939,6 +939,57 @@ describe('DiscordChannel', () => { }); }); + // --- editMessage --- + + describe('editMessage', () => { + it('edits in place when text fits in one message', async () => { + const opts = createTestOpts(); + const channel = new DiscordChannel('test-token', opts); + await channel.connect(); + + const msg = { edit: vi.fn().mockResolvedValue(undefined) }; + const mockChannel = { + send: vi.fn().mockResolvedValue(undefined), + messages: { fetch: vi.fn().mockResolvedValue(msg) }, + }; + currentClient().channels.fetch.mockResolvedValue(mockChannel); + + await channel.editMessage('dc:1234567890123456', 'msg_1', 'short text'); + + expect(msg.edit).toHaveBeenCalledTimes(1); + expect(mockChannel.send).not.toHaveBeenCalled(); + }); + + it('edits first chunk and appends the rest when text exceeds 2000 chars', async () => { + const opts = createTestOpts(); + const channel = new DiscordChannel('test-token', opts); + await channel.connect(); + + const msg = { edit: vi.fn().mockResolvedValue(undefined) }; + const mockChannel = { + send: vi.fn().mockResolvedValue(undefined), + messages: { fetch: vi.fn().mockResolvedValue(msg) }, + }; + currentClient().channels.fetch.mockResolvedValue(mockChannel); + + const longText = 'x'.repeat(3000); + await channel.editMessage('dc:1234567890123456', 'msg_1', longText); + + // First chunk goes into the existing (tracked) message via edit. + expect(msg.edit).toHaveBeenCalledTimes(1); + expect((msg.edit.mock.calls[0][0] as string).length).toBeLessThanOrEqual( + 2000, + ); + // The overflow is appended as a follow-up message (one message cannot + // hold >2000 chars), so the edit never sends an oversized body. + expect(mockChannel.send).toHaveBeenCalledTimes(1); + expect(mockChannel.send).toHaveBeenCalledWith({ + content: 'x'.repeat(1000), + flags: 1 << 2, + }); + }); + }); + // --- ownsJid --- describe('ownsJid', () => { diff --git a/src/channels/discord.ts b/src/channels/discord.ts index 6d64a79..1c83e45 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -948,7 +948,23 @@ export class DiscordChannel implements Channel { // Dashboard callers opt out with rawMarkdown: true to preserve // intentional bold/italic section headers. const cleaned = options.rawMarkdown ? text : formatOutbound(text); - await msg.edit(cleaned); + // Discord rejects edits/messages longer than 2000 chars with + // "Invalid Form Body … Must be 2000 or fewer". A single message cannot + // hold the overflow, so edit the tracked message with the first chunk + // and append the remaining chunks as follow-up messages. + const MAX_LENGTH = 2000; + if (cleaned.length <= MAX_LENGTH) { + await msg.edit(cleaned); + } else { + const chunks = chunkForDiscord(cleaned, MAX_LENGTH); + await msg.edit(chunks[0]); + for (let i = 1; i < chunks.length; i++) { + await (channel as TextChannel).send({ + content: chunks[i], + flags: MessageFlags.SuppressEmbeds, + }); + } + } logger.info( { jid, diff --git a/src/message-turn-controller.ts b/src/message-turn-controller.ts index a9944b1..57a855e 100644 --- a/src/message-turn-controller.ts +++ b/src/message-turn-controller.ts @@ -596,15 +596,19 @@ export class MessageTurnController { return; } + // Capture the value before the await: a concurrent resetProgressState() + // can null `latestProgressText` while editMessage is in flight, which used + // to throw "null is not an object (evaluating 'this.latestProgressText.length')". + const progressText = this.latestProgressText; if ( !this.progressMessageId || !this.options.channel.editMessage || - !this.latestProgressText + !progressText ) { return; } - const rendered = this.renderProgressMessage(this.latestProgressText); + const rendered = this.renderProgressMessage(progressText); try { await this.options.channel.editMessage( @@ -616,7 +620,7 @@ export class MessageTurnController { this.progressEditFailCount = 0; this.logOutboundAudit('progress-edit', { messageId: this.progressMessageId, - textLength: this.latestProgressText.length, + textLength: progressText.length, renderedLength: rendered.length, }); } catch (err) {