feat(signal): default Signal read receipts to true (#576)
Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
@@ -92,6 +92,7 @@ channels:
|
|||||||
signal:
|
signal:
|
||||||
enabled: true
|
enabled: true
|
||||||
phone: "+1234567890"
|
phone: "+1234567890"
|
||||||
|
readReceipts: true
|
||||||
selfChat: true
|
selfChat: true
|
||||||
dmPolicy: pairing
|
dmPolicy: pairing
|
||||||
|
|
||||||
@@ -239,6 +240,7 @@ agents:
|
|||||||
channels:
|
channels:
|
||||||
signal:
|
signal:
|
||||||
phone: "+1234567890"
|
phone: "+1234567890"
|
||||||
|
readReceipts: true
|
||||||
selfChat: true
|
selfChat: true
|
||||||
whatsapp:
|
whatsapp:
|
||||||
enabled: true
|
enabled: true
|
||||||
@@ -521,6 +523,7 @@ For dedicated bot numbers (`selfChat: false`), onboarding defaults to **allowlis
|
|||||||
| Option | Type | Description |
|
| Option | Type | Description |
|
||||||
|--------|------|-------------|
|
|--------|------|-------------|
|
||||||
| `phone` | string | Phone number with + prefix |
|
| `phone` | string | Phone number with + prefix |
|
||||||
|
| `readReceipts` | boolean | Send read receipts for incoming messages (default: `true`) |
|
||||||
| `selfChat` | boolean | `true` = only "Note to Self" works |
|
| `selfChat` | boolean | `true` = only "Note to Self" works |
|
||||||
|
|
||||||
## Features Configuration
|
## Features Configuration
|
||||||
@@ -1052,6 +1055,7 @@ Reference:
|
|||||||
| `WHATSAPP_ENABLED` | `channels.whatsapp.enabled` |
|
| `WHATSAPP_ENABLED` | `channels.whatsapp.enabled` |
|
||||||
| `WHATSAPP_SELF_CHAT_MODE` | `channels.whatsapp.selfChat` |
|
| `WHATSAPP_SELF_CHAT_MODE` | `channels.whatsapp.selfChat` |
|
||||||
| `SIGNAL_PHONE_NUMBER` | `channels.signal.phone` |
|
| `SIGNAL_PHONE_NUMBER` | `channels.signal.phone` |
|
||||||
|
| `SIGNAL_READ_RECEIPTS` | `channels.signal.readReceipts` |
|
||||||
| `OPENAI_API_KEY` | `transcription.apiKey` |
|
| `OPENAI_API_KEY` | `transcription.apiKey` |
|
||||||
| `GMAIL_ACCOUNT` | `polling.gmail.account` (comma-separated list allowed) |
|
| `GMAIL_ACCOUNT` | `polling.gmail.account` (comma-separated list allowed) |
|
||||||
| `POLLING_INTERVAL_MS` | `polling.intervalMs` |
|
| `POLLING_INTERVAL_MS` | `polling.intervalMs` |
|
||||||
|
|||||||
@@ -79,6 +79,9 @@ SIGNAL_PHONE_NUMBER=+17075204676
|
|||||||
|
|
||||||
# Optional: Self-chat mode for "Note to Self" (default: true)
|
# Optional: Self-chat mode for "Note to Self" (default: true)
|
||||||
# SIGNAL_SELF_CHAT_MODE=true
|
# SIGNAL_SELF_CHAT_MODE=true
|
||||||
|
|
||||||
|
# Optional: Send read receipts for incoming messages (default: true)
|
||||||
|
# SIGNAL_READ_RECEIPTS=true
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** For personal numbers (`selfChatMode: true`), `dmPolicy` is ignored - only you can message via "Note to Self". For dedicated bot numbers, onboarding defaults to `allowlist`.
|
**Note:** For personal numbers (`selfChatMode: true`), `dmPolicy` is ignored - only you can message via "Note to Self". For dedicated bot numbers, onboarding defaults to `allowlist`.
|
||||||
@@ -97,6 +100,7 @@ The daemon runs on port 8090 by default to avoid conflicts with other services.
|
|||||||
- **Direct Messages** - Receive and respond to DMs
|
- **Direct Messages** - Receive and respond to DMs
|
||||||
- **Note to Self** - Use Signal's "Note to Self" feature to message yourself (selfChatMode)
|
- **Note to Self** - Use Signal's "Note to Self" feature to message yourself (selfChatMode)
|
||||||
- **Allowlist** - For dedicated numbers, only pre-approved phone numbers can message
|
- **Allowlist** - For dedicated numbers, only pre-approved phone numbers can message
|
||||||
|
- **Read Receipts** - Enabled by default (disable with `SIGNAL_READ_RECEIPTS=false`)
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ const SHARED_CHANNEL_BUILDERS: SharedChannelBuilder[] = [
|
|||||||
cliPath: signal.cliPath || process.env.SIGNAL_CLI_PATH || 'signal-cli',
|
cliPath: signal.cliPath || process.env.SIGNAL_CLI_PATH || 'signal-cli',
|
||||||
httpHost: signal.httpHost || process.env.SIGNAL_HTTP_HOST || '127.0.0.1',
|
httpHost: signal.httpHost || process.env.SIGNAL_HTTP_HOST || '127.0.0.1',
|
||||||
httpPort: signal.httpPort || parseInt(process.env.SIGNAL_HTTP_PORT || '8090', 10),
|
httpPort: signal.httpPort || parseInt(process.env.SIGNAL_HTTP_PORT || '8090', 10),
|
||||||
|
readReceipts: signal.readReceipts ?? (process.env.SIGNAL_READ_RECEIPTS !== 'false'),
|
||||||
dmPolicy: signal.dmPolicy || 'pairing',
|
dmPolicy: signal.dmPolicy || 'pairing',
|
||||||
allowedUsers: nonEmpty(signal.allowedUsers),
|
allowedUsers: nonEmpty(signal.allowedUsers),
|
||||||
selfChatMode,
|
selfChatMode,
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { SignalAdapter } from './signal.js';
|
import { SignalAdapter } from './signal.js';
|
||||||
|
|
||||||
|
type SignalAdapterWithInternals = {
|
||||||
|
config: {
|
||||||
|
readReceipts?: boolean;
|
||||||
|
};
|
||||||
|
buildDaemonArgs: () => string[];
|
||||||
|
};
|
||||||
|
|
||||||
describe('SignalAdapter sendFile', () => {
|
describe('SignalAdapter sendFile', () => {
|
||||||
function createAdapter(phone = '+15555555555') {
|
function createAdapter(phone = '+15555555555') {
|
||||||
return new SignalAdapter({ phoneNumber: phone });
|
return new SignalAdapter({ phoneNumber: phone });
|
||||||
@@ -87,3 +94,22 @@ describe('SignalAdapter sendFile', () => {
|
|||||||
expect(result.messageId).toBe('unknown');
|
expect(result.messageId).toBe('unknown');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('SignalAdapter read receipts', () => {
|
||||||
|
it('defaults readReceipts to true', () => {
|
||||||
|
const adapter = new SignalAdapter({ phoneNumber: '+15555555555' });
|
||||||
|
const internal = adapter as unknown as SignalAdapterWithInternals;
|
||||||
|
expect(internal.config.readReceipts).toBe(true);
|
||||||
|
expect(internal.buildDaemonArgs()).toContain('--send-read-receipts');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('omits daemon read receipts flag when disabled', () => {
|
||||||
|
const adapter = new SignalAdapter({
|
||||||
|
phoneNumber: '+15555555555',
|
||||||
|
readReceipts: false,
|
||||||
|
});
|
||||||
|
const internal = adapter as unknown as SignalAdapterWithInternals;
|
||||||
|
expect(internal.config.readReceipts).toBe(false);
|
||||||
|
expect(internal.buildDaemonArgs()).not.toContain('--send-read-receipts');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ export interface SignalConfig {
|
|||||||
httpHost?: string; // Daemon HTTP host (default: "127.0.0.1")
|
httpHost?: string; // Daemon HTTP host (default: "127.0.0.1")
|
||||||
httpPort?: number; // Daemon HTTP port (default: 8090)
|
httpPort?: number; // Daemon HTTP port (default: 8090)
|
||||||
startupTimeoutMs?: number; // Max time to wait for daemon startup (default: 30000)
|
startupTimeoutMs?: number; // Max time to wait for daemon startup (default: 30000)
|
||||||
|
readReceipts?: boolean; // Send read receipts for incoming messages (default: true)
|
||||||
// Security
|
// Security
|
||||||
dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open'
|
dmPolicy?: DmPolicy; // 'pairing' (default), 'allowlist', or 'open'
|
||||||
allowedUsers?: string[]; // Phone numbers (config allowlist)
|
allowedUsers?: string[]; // Phone numbers (config allowlist)
|
||||||
@@ -171,6 +172,7 @@ export class SignalAdapter implements ChannelAdapter {
|
|||||||
this.config = {
|
this.config = {
|
||||||
...config,
|
...config,
|
||||||
dmPolicy: config.dmPolicy || 'pairing',
|
dmPolicy: config.dmPolicy || 'pairing',
|
||||||
|
readReceipts: config.readReceipts !== false, // Default true
|
||||||
selfChatMode: config.selfChatMode !== false, // Default true
|
selfChatMode: config.selfChatMode !== false, // Default true
|
||||||
};
|
};
|
||||||
const host = config.httpHost || '127.0.0.1';
|
const host = config.httpHost || '127.0.0.1';
|
||||||
@@ -394,21 +396,28 @@ This code expires in 1 hour.`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
// --- Private methods ---
|
// --- Private methods ---
|
||||||
|
|
||||||
private async startDaemon(): Promise<void> {
|
private buildDaemonArgs(): string[] {
|
||||||
const cliPath = this.config.cliPath || 'signal-cli';
|
|
||||||
const host = this.config.httpHost || '127.0.0.1';
|
|
||||||
const port = this.config.httpPort || 8090;
|
|
||||||
|
|
||||||
const args: string[] = [];
|
const args: string[] = [];
|
||||||
|
|
||||||
if (this.config.phoneNumber) {
|
if (this.config.phoneNumber) {
|
||||||
args.push('-a', this.config.phoneNumber);
|
args.push('-a', this.config.phoneNumber);
|
||||||
}
|
}
|
||||||
|
|
||||||
args.push('daemon');
|
args.push('daemon');
|
||||||
args.push('--http', `${host}:${port}`);
|
args.push('--http', `${this.config.httpHost || '127.0.0.1'}:${this.config.httpPort || 8090}`);
|
||||||
args.push('--no-receive-stdout');
|
args.push('--no-receive-stdout');
|
||||||
|
|
||||||
|
if (this.config.readReceipts !== false) {
|
||||||
|
args.push('--send-read-receipts');
|
||||||
|
}
|
||||||
|
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async startDaemon(): Promise<void> {
|
||||||
|
const cliPath = this.config.cliPath || 'signal-cli';
|
||||||
|
const args = this.buildDaemonArgs();
|
||||||
|
|
||||||
log.info(`Spawning: ${cliPath} ${args.join(' ')}`);
|
log.info(`Spawning: ${cliPath} ${args.join(' ')}`);
|
||||||
|
|
||||||
|
|||||||
@@ -379,6 +379,10 @@ export function configToEnv(config: LettaBotConfig): Record<string, string> {
|
|||||||
}
|
}
|
||||||
if (config.channels.signal?.phone) {
|
if (config.channels.signal?.phone) {
|
||||||
env.SIGNAL_PHONE_NUMBER = config.channels.signal.phone;
|
env.SIGNAL_PHONE_NUMBER = config.channels.signal.phone;
|
||||||
|
// Signal readReceipts defaults to true, so only set env if explicitly false
|
||||||
|
if (config.channels.signal.readReceipts === false) {
|
||||||
|
env.SIGNAL_READ_RECEIPTS = 'false';
|
||||||
|
}
|
||||||
// Signal selfChat defaults to true, so only set env if explicitly false
|
// Signal selfChat defaults to true, so only set env if explicitly false
|
||||||
if (config.channels.signal.selfChat === false) {
|
if (config.channels.signal.selfChat === false) {
|
||||||
env.SIGNAL_SELF_CHAT_MODE = 'false';
|
env.SIGNAL_SELF_CHAT_MODE = 'false';
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ describe('normalizeAgents', () => {
|
|||||||
'TELEGRAM_BOT_TOKEN', 'TELEGRAM_DM_POLICY', 'TELEGRAM_ALLOWED_USERS',
|
'TELEGRAM_BOT_TOKEN', 'TELEGRAM_DM_POLICY', 'TELEGRAM_ALLOWED_USERS',
|
||||||
'SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN', 'SLACK_DM_POLICY', 'SLACK_ALLOWED_USERS',
|
'SLACK_BOT_TOKEN', 'SLACK_APP_TOKEN', 'SLACK_DM_POLICY', 'SLACK_ALLOWED_USERS',
|
||||||
'WHATSAPP_ENABLED', 'WHATSAPP_SELF_CHAT_MODE', 'WHATSAPP_DM_POLICY', 'WHATSAPP_ALLOWED_USERS',
|
'WHATSAPP_ENABLED', 'WHATSAPP_SELF_CHAT_MODE', 'WHATSAPP_DM_POLICY', 'WHATSAPP_ALLOWED_USERS',
|
||||||
'SIGNAL_PHONE_NUMBER', 'SIGNAL_SELF_CHAT_MODE', 'SIGNAL_DM_POLICY', 'SIGNAL_ALLOWED_USERS',
|
'SIGNAL_PHONE_NUMBER', 'SIGNAL_SELF_CHAT_MODE', 'SIGNAL_READ_RECEIPTS', 'SIGNAL_DM_POLICY', 'SIGNAL_ALLOWED_USERS',
|
||||||
'DISCORD_BOT_TOKEN', 'DISCORD_DM_POLICY', 'DISCORD_ALLOWED_USERS',
|
'DISCORD_BOT_TOKEN', 'DISCORD_DM_POLICY', 'DISCORD_ALLOWED_USERS',
|
||||||
'BLUESKY_WANTED_DIDS', 'BLUESKY_WANTED_COLLECTIONS', 'BLUESKY_JETSTREAM_URL', 'BLUESKY_CURSOR',
|
'BLUESKY_WANTED_DIDS', 'BLUESKY_WANTED_COLLECTIONS', 'BLUESKY_JETSTREAM_URL', 'BLUESKY_CURSOR',
|
||||||
'BLUESKY_HANDLE', 'BLUESKY_APP_PASSWORD', 'BLUESKY_SERVICE_URL', 'BLUESKY_APPVIEW_URL',
|
'BLUESKY_HANDLE', 'BLUESKY_APP_PASSWORD', 'BLUESKY_SERVICE_URL', 'BLUESKY_APPVIEW_URL',
|
||||||
@@ -594,9 +594,25 @@ describe('normalizeAgents', () => {
|
|||||||
expect(agents[0].channels.slack?.appToken).toBe('slack-app');
|
expect(agents[0].channels.slack?.appToken).toBe('slack-app');
|
||||||
expect(agents[0].channels.whatsapp?.enabled).toBe(true);
|
expect(agents[0].channels.whatsapp?.enabled).toBe(true);
|
||||||
expect(agents[0].channels.signal?.phone).toBe('+1234567890');
|
expect(agents[0].channels.signal?.phone).toBe('+1234567890');
|
||||||
|
expect(agents[0].channels.signal?.readReceipts).toBe(true);
|
||||||
expect(agents[0].channels.discord?.token).toBe('discord-token');
|
expect(agents[0].channels.discord?.token).toBe('discord-token');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow disabling Signal read receipts via env var', () => {
|
||||||
|
process.env.SIGNAL_PHONE_NUMBER = '+1234567890';
|
||||||
|
process.env.SIGNAL_READ_RECEIPTS = 'false';
|
||||||
|
|
||||||
|
const config: LettaBotConfig = {
|
||||||
|
server: { mode: 'cloud' },
|
||||||
|
agent: { name: 'TestBot', model: 'test' },
|
||||||
|
channels: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
const agents = normalizeAgents(config);
|
||||||
|
|
||||||
|
expect(agents[0].channels.signal?.readReceipts).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('should pick up allowedUsers from env vars for all channels', () => {
|
it('should pick up allowedUsers from env vars for all channels', () => {
|
||||||
process.env.TELEGRAM_BOT_TOKEN = 'tg-token';
|
process.env.TELEGRAM_BOT_TOKEN = 'tg-token';
|
||||||
process.env.TELEGRAM_DM_POLICY = 'allowlist';
|
process.env.TELEGRAM_DM_POLICY = 'allowlist';
|
||||||
|
|||||||
@@ -374,6 +374,7 @@ export interface SignalConfig {
|
|||||||
cliPath?: string; // Path to signal-cli binary (default: "signal-cli")
|
cliPath?: string; // Path to signal-cli binary (default: "signal-cli")
|
||||||
httpHost?: string; // Daemon HTTP host (default: "127.0.0.1")
|
httpHost?: string; // Daemon HTTP host (default: "127.0.0.1")
|
||||||
httpPort?: number; // Daemon HTTP port (default: 8090)
|
httpPort?: number; // Daemon HTTP port (default: 8090)
|
||||||
|
readReceipts?: boolean; // Send read receipts for incoming messages (default: true)
|
||||||
selfChat?: boolean;
|
selfChat?: boolean;
|
||||||
dmPolicy?: 'pairing' | 'allowlist' | 'open';
|
dmPolicy?: 'pairing' | 'allowlist' | 'open';
|
||||||
allowedUsers?: string[];
|
allowedUsers?: string[];
|
||||||
@@ -707,6 +708,7 @@ export function normalizeAgents(config: LettaBotConfig): AgentConfig[] {
|
|||||||
channels.signal = {
|
channels.signal = {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
phone: process.env.SIGNAL_PHONE_NUMBER,
|
phone: process.env.SIGNAL_PHONE_NUMBER,
|
||||||
|
readReceipts: process.env.SIGNAL_READ_RECEIPTS !== 'false',
|
||||||
selfChat: process.env.SIGNAL_SELF_CHAT_MODE !== 'false',
|
selfChat: process.env.SIGNAL_SELF_CHAT_MODE !== 'false',
|
||||||
dmPolicy: (process.env.SIGNAL_DM_POLICY as 'pairing' | 'allowlist' | 'open') || 'pairing',
|
dmPolicy: (process.env.SIGNAL_DM_POLICY as 'pairing' | 'allowlist' | 'open') || 'pairing',
|
||||||
allowedUsers: parseList(process.env.SIGNAL_ALLOWED_USERS),
|
allowedUsers: parseList(process.env.SIGNAL_ALLOWED_USERS),
|
||||||
|
|||||||
Reference in New Issue
Block a user