From ae1b37694e94777d43f0721f84e8eb8f3effe9d2 Mon Sep 17 00:00:00 2001 From: Eyejoker Date: Sat, 14 Mar 2026 02:32:14 +0900 Subject: [PATCH] feat: inline text file attachments from Discord Download and inline content of .txt and other text-based file attachments instead of showing just the filename placeholder. Handles long copy-pastes that Discord auto-converts to .txt files. Truncates at 8000 chars with a note. --- src/channels/discord.test.ts | 13 +++++++++++-- src/channels/discord.ts | 29 +++++++++++++++++++++++++++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/src/channels/discord.test.ts b/src/channels/discord.test.ts index dc84cb3..d974b2e 100644 --- a/src/channels/discord.test.ts +++ b/src/channels/discord.test.ts @@ -504,6 +504,7 @@ describe('DiscordChannel', () => { vi.fn().mockResolvedValue({ ok: true, arrayBuffer: () => Promise.resolve(new ArrayBuffer(8)), + text: () => Promise.resolve('Hello from text file'), }), ); }); @@ -637,7 +638,15 @@ describe('DiscordChannel', () => { url: 'https://cdn.example.com/a.png', }, ], - ['att2', { name: 'b.txt', contentType: 'text/plain' }], + [ + 'att2', + { + id: 'att2', + name: 'b.txt', + contentType: 'text/plain', + url: 'https://cdn.example.com/b.txt', + }, + ], ]); const msg = createMessage({ content: '', @@ -650,7 +659,7 @@ describe('DiscordChannel', () => { 'dc:1234567890123456', expect.objectContaining({ content: expect.stringMatching( - /^\[Image: .+\.png\]\n\[File: b\.txt\]$/, + /^\[Image: .+\.png\]\n\[File: b\.txt\]\nHello from text file$/, ), }), ); diff --git a/src/channels/discord.ts b/src/channels/discord.ts index 1818b2b..f11fee2 100644 --- a/src/channels/discord.ts +++ b/src/channels/discord.ts @@ -93,8 +93,7 @@ async function transcribeAudio(att: Attachment): Promise { // Pick provider: Groq (fast) > OpenAI (fallback) const envVars = readEnvFile(['GROQ_API_KEY', 'OPENAI_API_KEY']); - const groqKey = - process.env.GROQ_API_KEY || envVars.GROQ_API_KEY || ''; + const groqKey = process.env.GROQ_API_KEY || envVars.GROQ_API_KEY || ''; const openaiKey = process.env.OPENAI_API_KEY || envVars.OPENAI_API_KEY || ''; @@ -267,6 +266,32 @@ export class DiscordChannel implements Channel { return `[Video: ${att.name || 'video'}]`; } else if (contentType.startsWith('audio/')) { return `[Audio: ${att.name || 'audio'}]`; + } else if ( + contentType.startsWith('text/') || + /\.(txt|md|json|csv|log|xml|yaml|yml|toml|ini|cfg|conf|sh|bash|zsh|py|js|ts|jsx|tsx|html|css|sql|rs|go|java|c|cpp|h|hpp|rb|php|swift|kt|scala|r|lua|pl|ex|exs|hs|ml|clj|dart|v|zig|nim|ps1|bat|cmd)$/i.test( + att.name || '', + ) + ) { + // Download and inline text-based files + try { + const res = await fetch(att.url); + if (!res.ok) throw new Error(`Download failed: ${res.status}`); + let text = await res.text(); + // Truncate very large files + const MAX_TEXT_LENGTH = 8000; + if (text.length > MAX_TEXT_LENGTH) { + text = + text.slice(0, MAX_TEXT_LENGTH) + + `\n...(truncated, ${text.length} chars total)`; + } + return `[File: ${att.name}]\n${text}`; + } catch (err) { + logger.error( + { err, file: att.name }, + 'Text file download failed', + ); + return `[File: ${att.name || 'file'} (download failed)]`; + } } else { return `[File: ${att.name || 'file'}]`; }