feat(channels): implement binary uploads for matrix signal mattermost

This commit is contained in:
William Valentin
2026-02-17 10:32:01 -08:00
parent e158968e03
commit 80a160a4eb
7 changed files with 290 additions and 22 deletions
+27
View File
@@ -68,6 +68,33 @@ describe('SignalAdapter', () => {
expect(sendCall?.[1]).toEqual(['-u', '+15551234567', 'send', '-m', 'Hello group', '-g', 'abcd1234']);
});
it('send uploads binary attachments via --attachment', async () => {
const adapter = new SignalAdapter({ account: '+15551234567' });
mockExecFileOnce((callback) => callback(null, 'signal-cli 0.13.2', ''));
mockExecFileOnce((callback) => callback(null, '', ''));
mockExecFileOnce((callback) => callback(null, '', ''));
await adapter.connect();
await adapter.send('+15550001111', {
text: '',
attachments: [{ mimeType: 'image/png', data: 'aGVsbG8=', filename: 'image.png' }],
});
await adapter.disconnect();
const attachmentCall = mockExecFile.mock.calls.find((call) => {
const args = call[1];
return Array.isArray(args) && args.includes('--attachment');
});
expect(attachmentCall).toBeDefined();
const args = attachmentCall?.[1] as string[];
expect(args[0]).toBe('-u');
expect(args[1]).toBe('+15551234567');
expect(args[2]).toBe('send');
expect(args[3]).toBe('--attachment');
expect(typeof args[4]).toBe('string');
expect(args[5]).toBe('+15550001111');
});
it('parses DM receive payload and forwards inbound message', async () => {
const adapter = new SignalAdapter({
account: '+15551234567',
+60 -2
View File
@@ -1,4 +1,7 @@
import { execFile } from 'child_process';
import { mkdtemp, rm, writeFile } from 'fs/promises';
import { tmpdir } from 'os';
import { join } from 'path';
import type {
InboundMessage,
@@ -63,6 +66,7 @@ export class SignalAdapter implements ChannelAdapter {
private readonly config: SignalAdapterConfig;
private pollTimer: NodeJS.Timeout | null = null;
private polling = false;
private attachmentTempCounter = 0;
get status(): ChannelStatus {
return this._status;
@@ -120,8 +124,7 @@ export class SignalAdapter implements ChannelAdapter {
const line = a.filename ? `${a.filename}: ${a.url}` : a.url;
await this.sendText(peerId, line);
} else if (a.data) {
// Keep adapter minimal and robust: no temp-file attachment upload in this pass.
console.warn(`Signal: skipping attachment data (${a.mimeType}) — upload not implemented`);
await this.sendBinaryAttachment(peerId, a.data, a.filename, a.mimeType);
}
}
}
@@ -141,6 +144,31 @@ export class SignalAdapter implements ChannelAdapter {
await this.execSignal(args);
}
private async sendBinaryAttachment(
peerId: string,
base64Data: string,
filename?: string,
mimeType?: string,
): Promise<void> {
const tempDir = await mkdtemp(join(tmpdir(), 'flynn-signal-'));
const ext = extensionFromMimeType(mimeType);
const safeName = sanitizeFilename(filename) || `attachment${ext}`;
const tempPath = join(tempDir, `${this.attachmentTempCounter++}-${safeName}`);
try {
await writeFile(tempPath, Buffer.from(base64Data, 'base64'));
const args = ['-u', this.config.account, 'send', '--attachment', tempPath];
const groupId = this.extractGroupId(peerId);
if (groupId) {
args.push('-g', groupId);
} else {
args.push(peerId);
}
await this.execSignal(args);
} finally {
await rm(tempDir, { recursive: true, force: true });
}
}
private async pollOnce(): Promise<void> {
if (this.polling || !this.messageHandler || this._status !== 'connected') {
return;
@@ -327,6 +355,36 @@ export class SignalAdapter implements ChannelAdapter {
}
}
function extensionFromMimeType(mimeType?: string): string {
if (!mimeType) {
return '.bin';
}
if (mimeType.startsWith('image/jpeg')) {
return '.jpg';
}
if (mimeType.startsWith('image/png')) {
return '.png';
}
if (mimeType.startsWith('image/gif')) {
return '.gif';
}
if (mimeType.startsWith('application/pdf')) {
return '.pdf';
}
const [, subtype] = mimeType.split('/');
if (!subtype) {
return '.bin';
}
return `.${subtype.split('+')[0]}`;
}
function sanitizeFilename(filename?: string): string {
if (!filename) {
return '';
}
return filename.replace(/[^a-zA-Z0-9._-]/g, '_');
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}