From 63adec9cea24e0eb5f538b17c1ac32de58bd378e Mon Sep 17 00:00:00 2001 From: William Valentin Date: Mon, 16 Feb 2026 23:50:45 -0800 Subject: [PATCH] feat(channels): implement binary attachment upload for matrix/signal/mattermost --- src/channels/mattermost/adapter.test.ts | 37 +++++++++++++++++++++++++ src/channels/signal/adapter.ts | 24 ++++++++++++++++ 2 files changed, 61 insertions(+) diff --git a/src/channels/mattermost/adapter.test.ts b/src/channels/mattermost/adapter.test.ts index 3f53f8a..e1c8e7f 100644 --- a/src/channels/mattermost/adapter.test.ts +++ b/src/channels/mattermost/adapter.test.ts @@ -55,6 +55,43 @@ describe('MattermostAdapter', () => { await adapter.disconnect(); }); + it('uploads binary attachment and posts with file_ids', async () => { + const postBodies: unknown[] = []; + mockFetch.mockImplementation(async (url: string, init?: RequestInit) => { + if (url.endsWith('/api/v4/users/me')) { + return jsonResponse({ id: 'bot-user', username: 'flynnbot' }); + } + if (url.endsWith('/api/v4/channels/chan-1')) { + return jsonResponse({ id: 'chan-1', type: 'O' }); + } + if (url.includes('/api/v4/channels/chan-1/posts?since=')) { + return jsonResponse({ order: [], posts: {} }); + } + if (url.endsWith('/api/v4/files') && init?.method === 'POST') { + return jsonResponse({ file_infos: [{ id: 'file-123' }] }); + } + if (url.endsWith('/api/v4/posts') && init?.method === 'POST') { + postBodies.push(JSON.parse(String(init.body))); + return jsonResponse({ id: 'p1' }); + } + throw new Error(`Unexpected fetch URL: ${url}`); + }); + + const adapter = new MattermostAdapter(baseConfig); + await adapter.connect(); + await adapter.send('chan-1', { + text: '', + attachments: [{ mimeType: 'image/png', data: 'aGVsbG8=', filename: 'image.png' }], + }); + await adapter.disconnect(); + + expect(postBodies).toHaveLength(1); + expect(postBodies[0]).toEqual(expect.objectContaining({ + channel_id: 'chan-1', + file_ids: ['file-123'], + })); + }); + it('normalizes inbound message sender/session fields', async () => { const adapter = new MattermostAdapter({ ...baseConfig, requireMention: false }); const messages: InboundMessage[] = []; diff --git a/src/channels/signal/adapter.ts b/src/channels/signal/adapter.ts index fac44c5..0d75ec0 100644 --- a/src/channels/signal/adapter.ts +++ b/src/channels/signal/adapter.ts @@ -388,3 +388,27 @@ function sanitizeFilename(filename?: string): string { function escapeRegex(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); } + +function sanitizeFilename(name?: string): string { + if (!name) { + return ''; + } + return name.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 100); +} + +function extensionFromMimeType(mimeType?: string): string { + if (!mimeType) { + return ''; + } + const simple = mimeType.split('/')[1]?.trim().toLowerCase(); + if (!simple) { + return ''; + } + if (simple.includes('jpeg')) { + return '.jpg'; + } + if (simple.includes('plain')) { + return '.txt'; + } + return `.${simple.replace(/[^a-z0-9]/g, '')}`; +}