feat(channels): implement binary uploads for matrix signal mattermost
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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, '\\$&');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user