This commit is contained in:
Ethan Stigter 2026-04-24 17:23:24 -06:00 committed by GitHub
commit 05d0b06dad
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 136 additions and 3 deletions

View file

@ -941,6 +941,9 @@ def _apply_env_overrides(config: GatewayConfig) -> None:
"account": signal_account,
"ignore_stories": os.getenv("SIGNAL_IGNORE_STORIES", "true").lower() in ("true", "1", "yes"),
})
signal_notify_self = os.getenv("SIGNAL_NOTIFY_SELF", "").lower()
if signal_notify_self in ("true", "1", "yes"):
config.platforms[Platform.SIGNAL].extra["notify_self"] = True
signal_home = os.getenv("SIGNAL_HOME_CHANNEL")
if signal_home and Platform.SIGNAL in config.platforms:
config.platforms[Platform.SIGNAL].home_channel = HomeChannel(

View file

@ -170,6 +170,7 @@ class SignalAdapter(BasePlatformAdapter):
self.http_url = extra.get("http_url", "http://127.0.0.1:8080").rstrip("/")
self.account = extra.get("account", "")
self.ignore_stories = extra.get("ignore_stories", True)
self.notify_self = extra.get("notify_self", False)
# Parse allowlists — group policy is derived from presence of group allowlist
group_allowed_str = os.getenv("SIGNAL_GROUP_ALLOWED_USERS", "")
@ -729,7 +730,12 @@ class SignalAdapter(BasePlatformAdapter):
if chat_id.startswith("group:"):
params["groupId"] = chat_id[6:]
else:
params["recipient"] = [await self._resolve_recipient(chat_id)]
recipient = await self._resolve_recipient(chat_id)
params["recipient"] = [recipient]
if self.notify_self and (
recipient == self._account_normalized or chat_id == self._account_normalized
):
params["notifySelf"] = True
result = await self._rpc("send", params)
@ -841,7 +847,12 @@ class SignalAdapter(BasePlatformAdapter):
if chat_id.startswith("group:"):
params["groupId"] = chat_id[6:]
else:
params["recipient"] = [await self._resolve_recipient(chat_id)]
recipient = await self._resolve_recipient(chat_id)
params["recipient"] = [recipient]
if self.notify_self and (
recipient == self._account_normalized or chat_id == self._account_normalized
):
params["notifySelf"] = True
result = await self._rpc("send", params)
if result is not None:
@ -880,7 +891,12 @@ class SignalAdapter(BasePlatformAdapter):
if chat_id.startswith("group:"):
params["groupId"] = chat_id[6:]
else:
params["recipient"] = [await self._resolve_recipient(chat_id)]
recipient = await self._resolve_recipient(chat_id)
params["recipient"] = [recipient]
if self.notify_self and (
recipient == self._account_normalized or chat_id == self._account_normalized
):
params["notifySelf"] = True
result = await self._rpc("send", params)
if result is not None:

View file

@ -57,6 +57,30 @@ class TestSignalConfigLoading:
assert sc.extra["http_url"] == "http://localhost:9090"
assert sc.extra["account"] == "+15551234567"
def test_apply_env_overrides_signal_notify_self(self, monkeypatch):
monkeypatch.setenv("SIGNAL_HTTP_URL", "http://localhost:9090")
monkeypatch.setenv("SIGNAL_ACCOUNT", "+15551234567")
monkeypatch.setenv("SIGNAL_NOTIFY_SELF", "true")
from gateway.config import GatewayConfig, _apply_env_overrides
config = GatewayConfig()
_apply_env_overrides(config)
sc = config.platforms[Platform.SIGNAL]
assert sc.extra.get("notify_self") is True
def test_apply_env_overrides_signal_notify_self_disabled(self, monkeypatch):
monkeypatch.setenv("SIGNAL_HTTP_URL", "http://localhost:9090")
monkeypatch.setenv("SIGNAL_ACCOUNT", "+15551234567")
monkeypatch.setenv("SIGNAL_NOTIFY_SELF", "false")
from gateway.config import GatewayConfig, _apply_env_overrides
config = GatewayConfig()
_apply_env_overrides(config)
sc = config.platforms[Platform.SIGNAL]
assert "notify_self" not in sc.extra
def test_signal_not_loaded_without_both_vars(self, monkeypatch):
monkeypatch.setenv("SIGNAL_HTTP_URL", "http://localhost:9090")
# No SIGNAL_ACCOUNT
@ -997,3 +1021,93 @@ class TestSignalTypingBackoff:
assert "+155****4567" not in adapter._typing_failures
assert "+155****4567" not in adapter._typing_skip_until
# ---------------------------------------------------------------------------
# notifySelf toggle for Note-to-Self bubble-side fix
# ---------------------------------------------------------------------------
class TestSignalNotifySelf:
"""Verify that notify_self=True adds notifySelf param for self-messages
in text, image, and attachment sends, while leaving other chats untouched."""
@pytest.mark.asyncio
async def test_send_to_self_adds_notifySelf_when_enabled(self, monkeypatch):
adapter = _make_signal_adapter(monkeypatch, notify_self=True)
mock_rpc, captured = _stub_rpc({"timestamp": 1234567890})
adapter._rpc = mock_rpc
adapter._stop_typing_indicator = AsyncMock()
result = await adapter.send(chat_id="+15551234567", content="hello self")
assert result.success is True
send_call = [c for c in captured if c["method"] == "send"][0]
assert send_call["params"].get("notifySelf") is True
@pytest.mark.asyncio
async def test_send_to_self_omits_notifySelf_when_disabled(self, monkeypatch):
adapter = _make_signal_adapter(monkeypatch, notify_self=False)
mock_rpc, captured = _stub_rpc({"timestamp": 1234567890})
adapter._rpc = mock_rpc
adapter._stop_typing_indicator = AsyncMock()
result = await adapter.send(chat_id="+15551234567", content="hello self")
assert result.success is True
send_call = [c for c in captured if c["method"] == "send"][0]
assert "notifySelf" not in send_call["params"]
@pytest.mark.asyncio
async def test_send_to_other_omits_notifySelf_even_when_enabled(self, monkeypatch):
adapter = _make_signal_adapter(monkeypatch, notify_self=True)
mock_rpc, captured = _stub_rpc({"timestamp": 1234567890})
adapter._rpc = mock_rpc
adapter._stop_typing_indicator = AsyncMock()
result = await adapter.send(chat_id="+15559999999", content="hello other")
assert result.success is True
send_call = [c for c in captured if c["method"] == "send"][0]
assert "notifySelf" not in send_call["params"]
@pytest.mark.asyncio
async def test_send_image_file_to_self_adds_notifySelf(self, monkeypatch, tmp_path):
adapter = _make_signal_adapter(monkeypatch, notify_self=True)
mock_rpc, captured = _stub_rpc({"timestamp": 1234567890})
adapter._rpc = mock_rpc
adapter._stop_typing_indicator = AsyncMock()
img_path = tmp_path / "chart.png"
img_path.write_bytes(b"\x89PNG" + b"\x00" * 100)
result = await adapter.send_image_file(chat_id="+15551234567", image_path=str(img_path))
assert result.success is True
send_call = [c for c in captured if c["method"] == "send"][0]
assert send_call["params"].get("notifySelf") is True
@pytest.mark.asyncio
async def test_send_voice_to_self_adds_notifySelf(self, monkeypatch, tmp_path):
adapter = _make_signal_adapter(monkeypatch, notify_self=True)
mock_rpc, captured = _stub_rpc({"timestamp": 1234567890})
adapter._rpc = mock_rpc
adapter._stop_typing_indicator = AsyncMock()
audio_path = tmp_path / "note.ogg"
audio_path.write_bytes(b"OggS" + b"\x00" * 100)
result = await adapter.send_voice(chat_id="+15551234567", audio_path=str(audio_path))
assert result.success is True
send_call = [c for c in captured if c["method"] == "send"][0]
assert send_call["params"].get("notifySelf") is True
@pytest.mark.asyncio
async def test_group_send_omits_notifySelf(self, monkeypatch, tmp_path):
adapter = _make_signal_adapter(monkeypatch, notify_self=True)
mock_rpc, captured = _stub_rpc({"timestamp": 1234567890})
adapter._rpc = mock_rpc
adapter._stop_typing_indicator = AsyncMock()
img_path = tmp_path / "chart.png"
img_path.write_bytes(b"\x89PNG" + b"\x00" * 100)
result = await adapter.send_image_file(chat_id="group:abc123==", image_path=str(img_path))
assert result.success is True
send_call = [c for c in captured if c["method"] == "send"][0]
assert "notifySelf" not in send_call["params"]