From 9aa2ab16578a74f1df9d1b6d57b3413bcf471bb3 Mon Sep 17 00:00:00 2001 From: Kaloyan Tenchov Date: Sat, 16 May 2026 11:33:30 -0400 Subject: [PATCH] feat(signal): make signal-cli attachments directory configurable The inbound attachment loop hardcoded ~/.local/share/signal-cli/attachments as the source path. That is the daemon's default on Linux but not on macOS or Windows, and breaks if the daemon was launched with XDG_DATA_HOME set. Add SignalConfig.attachments_dir as an optional override. When unset the behavior is unchanged; when set the value is run through Path.expanduser() so ~ is honored. Co-Authored-By: Claude Opus 4.7 --- nanobot/channels/signal.py | 21 +++++++++++++++++---- tests/channels/test_signal_channel.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+), 4 deletions(-) diff --git a/nanobot/channels/signal.py b/nanobot/channels/signal.py index fce941c74..96e515467 100644 --- a/nanobot/channels/signal.py +++ b/nanobot/channels/signal.py @@ -296,6 +296,11 @@ class SignalConfig(Base): daemon_host: str = "localhost" daemon_port: int = 8080 group_message_buffer_size: int = 20 # Number of recent group messages to keep for context + # Override the directory signal-cli writes inbound attachments to. When + # None, defaults to ~/.local/share/signal-cli/attachments (the daemon's + # platform default on Linux). Set this if the daemon is running with a + # custom XDG_DATA_HOME or on macOS/Windows where the default path differs. + attachments_dir: str | None = None dm: SignalDMConfig = Field(default_factory=SignalDMConfig) group: SignalGroupConfig = Field(default_factory=SignalGroupConfig) @@ -749,10 +754,7 @@ class SignalChannel(BaseChannel): continue try: - # signal-cli stores attachments in ~/.local/share/signal-cli/attachments/ - source_path = ( - Path.home() / ".local/share/signal-cli/attachments" / attachment_id - ) + source_path = self._signal_attachments_dir() / attachment_id if source_path.exists(): dest_path = media_dir / f"signal_{safe_filename(filename)}" @@ -864,6 +866,17 @@ class SignalChannel(BaseChannel): return "\n".join(lines) + def _signal_attachments_dir(self) -> Path: + """Return the directory signal-cli writes inbound attachments to. + + Defaults to ``~/.local/share/signal-cli/attachments`` (the daemon's + platform default on Linux) when ``config.attachments_dir`` is unset. + """ + configured = self.config.attachments_dir + if configured: + return Path(configured).expanduser() + return Path.home() / ".local/share/signal-cli/attachments" + @staticmethod def _normalize_signal_id(value: str) -> list[str]: """Normalize Signal identifiers (phone/uuid/service-id) for matching.""" diff --git a/tests/channels/test_signal_channel.py b/tests/channels/test_signal_channel.py index d6308b803..020118c4c 100644 --- a/tests/channels/test_signal_channel.py +++ b/tests/channels/test_signal_channel.py @@ -3,6 +3,7 @@ from __future__ import annotations import asyncio +from pathlib import Path from unittest.mock import AsyncMock import pytest @@ -71,6 +72,7 @@ def _make_channel( group_allow_from: list[str] | None = None, require_mention: bool = True, group_buffer_size: int = 20, + attachments_dir: str | None = None, ) -> SignalChannel: config = SignalConfig( enabled=True, @@ -87,6 +89,7 @@ def _make_channel( require_mention=require_mention, ), group_message_buffer_size=group_buffer_size, + attachments_dir=attachments_dir, ) return SignalChannel(config, MessageBus()) @@ -471,6 +474,21 @@ class TestGroupBuffer: # --------------------------------------------------------------------------- +class TestAttachmentsDir: + def test_default_attachments_dir(self): + ch = _make_channel() + expected = Path.home() / ".local/share/signal-cli/attachments" + assert ch._signal_attachments_dir() == expected + + def test_configured_attachments_dir(self, tmp_path): + ch = _make_channel(attachments_dir=str(tmp_path / "custom")) + assert ch._signal_attachments_dir() == tmp_path / "custom" + + def test_attachments_dir_expands_user(self): + ch = _make_channel(attachments_dir="~/signal-attachments") + assert ch._signal_attachments_dir() == Path.home() / "signal-attachments" + + class TestHandleDataMessageDM: def _make_dm_channel(self, policy="open", allow_from=None) -> tuple[SignalChannel, list]: ch = _make_channel(dm_enabled=True, dm_policy=policy, dm_allow_from=allow_from or [])