feat(channels): implement binary attachment upload for matrix/signal/mattermost

This commit is contained in:
William Valentin
2026-02-16 23:50:45 -08:00
parent 18da9ddf90
commit 63adec9cea
2 changed files with 61 additions and 0 deletions
+37
View File
@@ -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[] = [];
+24
View File
@@ -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, '')}`;
}