312 lines
10 KiB
Markdown
312 lines
10 KiB
Markdown
# Add Signal Channel (v2)
|
||
|
||
Adds Signal messaging support to NanoClaw v2 using `signal-sdk` (a TypeScript
|
||
wrapper around `signal-cli`). Unlike Telegram/Discord, Signal has no bot API —
|
||
NanoClaw registers as a full Signal account on a dedicated phone number.
|
||
|
||
**Two registration paths:**
|
||
- **New number (recommended):** Register a dedicated SIM or VoIP number as a
|
||
standalone Signal account. NanoClaw owns the number entirely.
|
||
- **Linked device:** Join an existing Signal account as a secondary device via
|
||
QR code. Simpler, but NanoClaw shares your personal number.
|
||
|
||
Both paths are documented below. The new-number path is battle-tested.
|
||
|
||
---
|
||
|
||
## Pre-flight
|
||
|
||
Check if `src/channels/signal.ts` exists and the import is uncommented in
|
||
`src/channels/index.ts`. If both are in place, skip to Registration.
|
||
|
||
## Install
|
||
|
||
### 1. Check Java
|
||
|
||
Java 17+ is required. Check:
|
||
|
||
```bash
|
||
java -version
|
||
```
|
||
|
||
If missing:
|
||
- **RHEL/CentOS/Fedora:** `sudo dnf install -y java-17-openjdk`
|
||
- **Debian/Ubuntu:** `sudo apt-get install -y default-jre`
|
||
- **macOS:** `brew install --cask temurin@17`
|
||
|
||
Java 17–25 all work. Java 25 (RHEL9 default) is confirmed working.
|
||
|
||
### 2. Install signal-cli
|
||
|
||
The `signal-sdk` npm package bundles signal-cli, but the bundled version is
|
||
often outdated. Install the latest standalone binary:
|
||
|
||
```bash
|
||
SIGNAL_CLI_VERSION=$(curl -fsSL https://api.github.com/repos/AsamK/signal-cli/releases/latest | python3 -c "import sys,json; print(json.load(sys.stdin)['tag_name'][1:])")
|
||
curl -fsSL "https://github.com/AsamK/signal-cli/releases/download/v${SIGNAL_CLI_VERSION}/signal-cli-${SIGNAL_CLI_VERSION}-Linux-native.tar.gz" \
|
||
| tar -xz -C ~/.local
|
||
ln -sf ~/.local/signal-cli ~/.local/bin/signal-cli
|
||
signal-cli --version
|
||
```
|
||
|
||
> **Note:** The Linux native tarball extracts a single binary directly to
|
||
> `~/.local/signal-cli` (not into a subdirectory). The symlink above handles this.
|
||
|
||
### 3. Install signal-sdk
|
||
|
||
```bash
|
||
npm install signal-sdk
|
||
```
|
||
|
||
### 4. Enable the adapter
|
||
|
||
Uncomment the Signal import in `src/channels/index.ts`:
|
||
|
||
```typescript
|
||
import './signal.js';
|
||
```
|
||
|
||
### 5. Build
|
||
|
||
Always build with Node 22 (nvm):
|
||
|
||
```bash
|
||
source ~/.nvm/nvm.sh && nvm use 22 && npm run build
|
||
```
|
||
|
||
---
|
||
|
||
## Path A: Register a new Signal number
|
||
|
||
Use this if you have a dedicated SIM or VoIP number for NanoClaw.
|
||
|
||
> **VoIP numbers:** Signal requires SMS verification before voice. Some VoIP
|
||
> providers are blocked even for voice calls. If registration fails with an auth
|
||
> error, try a different provider or a physical SIM.
|
||
|
||
### Step 1: Request SMS verification
|
||
|
||
Signal requires a CAPTCHA on first registration:
|
||
|
||
1. Open `https://signalcaptchas.org/registration/generate.html` in a browser
|
||
2. Solve the captcha
|
||
3. Right-click the **"Open Signal"** button → **Copy Link**
|
||
4. The link starts with `signalcaptcha://...`
|
||
|
||
```bash
|
||
SIGNAL_CLI_CONFIG_PATH=/path/to/nanoclaw/data/signal \
|
||
signal-cli -u +YOURNUMBER register \
|
||
--captcha "PASTE_CAPTCHA_TOKEN_HERE"
|
||
```
|
||
|
||
The captcha token is everything after `signalcaptcha://` in the copied link.
|
||
|
||
### Step 2: Voice call fallback (VoIP numbers without SMS)
|
||
|
||
If your number cannot receive SMS, wait ~60 seconds after the SMS request then
|
||
request a voice call:
|
||
|
||
```bash
|
||
SIGNAL_CLI_CONFIG_PATH=/path/to/nanoclaw/data/signal \
|
||
signal-cli -u +YOURNUMBER register --voice \
|
||
--captcha "SAME_CAPTCHA_TOKEN"
|
||
```
|
||
|
||
Signal will call your number and read a 6-digit code.
|
||
|
||
> The captcha token from Step 1 is reusable for the voice retry — no need to
|
||
> solve a new one.
|
||
|
||
### Step 3: Verify
|
||
|
||
```bash
|
||
SIGNAL_CLI_CONFIG_PATH=/path/to/nanoclaw/data/signal \
|
||
signal-cli -u +YOURNUMBER verify CODE
|
||
```
|
||
|
||
No output = success.
|
||
|
||
### Step 4: Set profile name (optional)
|
||
|
||
> ⚠ signal-sdk holds an exclusive lock on `data/signal/` while nanoclaw is
|
||
> running. Stop the service before running signal-cli commands, then restart.
|
||
|
||
```bash
|
||
systemctl --user stop nanoclaw
|
||
SIGNAL_CLI_CONFIG_PATH=/path/to/nanoclaw/data/signal \
|
||
signal-cli -u +YOURNUMBER updateProfile --name "YourBotName"
|
||
systemctl --user start nanoclaw
|
||
```
|
||
|
||
To set an avatar too:
|
||
```bash
|
||
signal-cli -u +YOURNUMBER updateProfile --name "YourBotName" --avatar /path/to/avatar.jpg
|
||
```
|
||
|
||
---
|
||
|
||
## Path B: Link as secondary device
|
||
|
||
Use this to join an existing Signal account as a secondary device.
|
||
|
||
```bash
|
||
mkdir -p data/signal
|
||
export SIGNAL_CLI_CONFIG_PATH=$(pwd)/data/signal
|
||
npx signal-sdk link -n "NanoClaw" -a +YOURNUMBER
|
||
```
|
||
|
||
This prints a QR code. On your phone: **Settings → Linked Devices → Link New Device**.
|
||
Scan the code within ~30 seconds.
|
||
|
||
---
|
||
|
||
## Configure environment
|
||
|
||
Add to `.env`:
|
||
|
||
```bash
|
||
SIGNAL_PHONE_NUMBER=+YOURNUMBER
|
||
```
|
||
|
||
---
|
||
|
||
## Wire to an agent
|
||
|
||
### DMs
|
||
|
||
After the service starts, **send any message** to the Signal number from your
|
||
personal Signal app. The router auto-creates a `messaging_groups` row. Then:
|
||
|
||
```bash
|
||
sqlite3 data/v2.db \
|
||
"SELECT id, platform_id FROM messaging_groups WHERE channel_type='signal' ORDER BY created_at DESC LIMIT 5"
|
||
```
|
||
|
||
Pass the `id` and `platform_id` to `/init-first-agent` or wire manually.
|
||
|
||
**Important:** DM `platform_id` is UUID-based, not phone-based:
|
||
- DM: `signal:3de71d7f-ffa3-437e-b4db-097534bccd46` (UUID of sender)
|
||
- Group: `signal:UwaIz6bc09Olg1pBr/XeBuQf6z3fyCZoNj/Y3Tpe3hI=` (base64 group ID)
|
||
|
||
### Groups
|
||
|
||
Add the Signal number to a group from your phone. Send any message — the router
|
||
auto-creates the group's `messaging_groups` row. Wire it:
|
||
|
||
```bash
|
||
sqlite3 data/v2.db \
|
||
"SELECT id, platform_id FROM messaging_groups WHERE channel_type='signal' ORDER BY created_at DESC LIMIT 5"
|
||
```
|
||
|
||
Then insert the wiring (replace IDs):
|
||
|
||
```bash
|
||
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||
sqlite3 data/v2.db "
|
||
INSERT OR IGNORE INTO messaging_group_agents
|
||
(id, messaging_group_id, agent_group_id, session_mode, priority, created_at)
|
||
VALUES
|
||
('mga-'||hex(randomblob(8)), 'mg-GROUPID', 'ag-AGENTID', 'isolated', 0, '$NOW');
|
||
"
|
||
```
|
||
|
||
Use `session_mode='isolated'` for groups so each group has its own session.
|
||
|
||
### Grant user access
|
||
|
||
Users who message via Signal need to be granted membership. Without this,
|
||
messages are silently dropped with `not_member`. After first contact:
|
||
|
||
```bash
|
||
NOW=$(date -u +"%Y-%m-%dT%H:%M:%S.000Z")
|
||
sqlite3 data/v2.db "
|
||
INSERT OR REPLACE INTO user_roles (user_id, role, agent_group_id, granted_by, granted_at)
|
||
VALUES ('signal:UUID', 'owner', NULL, 'system', '$NOW');
|
||
INSERT OR IGNORE INTO agent_group_members (user_id, agent_group_id, added_by, added_at)
|
||
VALUES ('signal:UUID', 'ag-AGENTID', 'system', '$NOW');
|
||
"
|
||
```
|
||
|
||
Find the UUID from `messaging_groups.platform_id` or the `users` table.
|
||
|
||
---
|
||
|
||
## Voice Transcription (optional)
|
||
|
||
Inbound voice messages are automatically transcribed using a local whisper
|
||
binary. The feature degrades gracefully — if whisper or ffmpeg is missing,
|
||
voice messages are still delivered as attachments with no transcript.
|
||
|
||
See `/add-voice-transcription-free-whisper` for full setup instructions
|
||
(ffmpeg, whisper backends, model download, environment variables, troubleshooting).
|
||
|
||
---
|
||
|
||
## Channel Info
|
||
|
||
- **type**: `signal`
|
||
- **terminology**: "Chats" (1:1) and "groups" (multi-member). No threads.
|
||
- **supports-threads**: no
|
||
- **inbound**: text, reactions (forwarded to agent with emoji + targetTimestamp), images, files, voice (transcribed via whisper-cli if installed)
|
||
- **outbound**: text, reactions (sendReaction), file attachments
|
||
- **platform-id-format**:
|
||
- DM: `signal:{UUID}` — sender's Signal UUID, **not** their phone number
|
||
- Group: `signal:{base64GroupId}`
|
||
- **how-to-find-id**: Send a message to the bot, then query `messaging_groups`
|
||
as shown above.
|
||
- **config-lock**: signal-sdk holds an exclusive lock on `data/signal/` while
|
||
nanoclaw is running. Stop the service before running any `signal-cli` commands.
|
||
- **attachment storage**: signal-sdk launches signal-cli **without** a `--config`
|
||
flag, so signal-cli stores attachments at the XDG default
|
||
(`~/.local/share/signal-cli/attachments/`), not under `data/signal/`. The
|
||
adapter checks both locations. Verify with:
|
||
`ps aux | grep signal-cli` — if there is no `-c` argument, XDG default is in use.
|
||
|
||
---
|
||
|
||
## Troubleshooting
|
||
|
||
**`Config file is in use by another instance`** — nanoclaw is running and
|
||
signal-sdk has the lock. Stop the service, run the command, restart.
|
||
|
||
**Messages dropped with `not_member`** — the Signal user hasn't been granted
|
||
membership. See "Grant user access" above. This affects every new Signal user,
|
||
including the owner's Signal identity (which is separate from their Telegram
|
||
identity even if it's the same person).
|
||
|
||
**Captcha required** — Signal requires captcha for new registrations. Go to
|
||
`https://signalcaptchas.org/registration/generate.html`, solve it, right-click
|
||
"Open Signal", copy the link, extract the token after `signalcaptcha://`.
|
||
|
||
**`Invalid verification method: Before requesting voice verification…`** —
|
||
You must request SMS first, wait ~60 seconds, then request voice. Both can use
|
||
the same captcha token.
|
||
|
||
**`The provided model identifier is invalid` (Bedrock)** — unrelated to Signal;
|
||
this is a LiteLLM model ID issue.
|
||
|
||
**Group replies going to DM instead of group** — modern Signal groups use
|
||
GroupV2. The adapter must extract the group ID from
|
||
`envelope?.dataMessage?.groupV2?.id` (not just `groupInfo?.groupId`, which is
|
||
GroupV1/legacy). Check `src/channels/signal.ts` and confirm the groupId
|
||
extraction falls through to `groupV2.id`.
|
||
|
||
**Voice messages / attachments silently skipped, no transcript** — signal-sdk
|
||
launches signal-cli without a `--config` flag, so attachments land at the XDG
|
||
default (`~/.local/share/signal-cli/attachments/`) rather than under
|
||
`data/signal/`. Confirm with `ps aux | grep signal-cli` — if there is no `-c`
|
||
argument in the process line, the XDG default is in use. The adapter falls back
|
||
to that location automatically. If you still see no "Signal attachment saved"
|
||
log lines, add a debug log around the `if (!storedPath) continue` guard in
|
||
`src/channels/signal.ts` to inspect `att.storedFilename` and `att.id`.
|
||
|
||
**Java not found** — install Java 17+ (see Install step 1).
|
||
|
||
**QR code expired (Path B)** — QR codes expire in ~30 seconds. Re-run the
|
||
link command to generate a new one.
|
||
|
||
**signal-cli binary location** — The native Linux tarball extracts directly to
|
||
`~/.local/signal-cli` (a single file, not a directory). The system `aws` or
|
||
other tools named `signal-cli` won't be in PATH by default; check
|
||
`~/.local/bin/signal-cli`.
|