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 <noreply@anthropic.com>
This commit is contained in:
Kaloyan Tenchov 2026-05-16 11:33:30 -04:00 committed by chengyongru
parent 971b774282
commit 9aa2ab1657
2 changed files with 35 additions and 4 deletions

View File

@ -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."""

View File

@ -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 [])