Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 33 additions & 4 deletions src/blacki/telegram/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -340,21 +340,27 @@ def _get_access_storage(self) -> TelegramAccessStorage:

async def _authorize_update(self, update: Update) -> bool:
"""Allow only authenticated private Telegram traffic into the bot."""
if not self.config.access_control_enabled:
return True

message = update.message
callback = update.callback_query
if message is None and callback is not None:
message = callback.message

sender = (
callback.from_user
if callback is not None
else (message.from_user if message is not None else None)
)
if not self.config.access_control_enabled:
await self._record_identity_if_private(message, sender)
return True

if message is None or message.chat.type != ChatType.PRIVATE:
if callback is not None:
await self.api.answer_callback_query(
callback.id, text="Access required"
)
return False

sender = callback.from_user if callback is not None else message.from_user
if sender is None or sender.id != message.chat.id:
return False

Expand All @@ -380,6 +386,29 @@ async def _authorize_update(self, update: Update) -> bool:
logger.exception("Telegram access control failed closed")
return False

async def _record_identity_if_private(
self,
message: Message | None,
sender: User | None,
) -> None:
"""Persist a direct-chat sender label when local storage is available."""
if (
message is None
or message.chat.type != ChatType.PRIVATE
or sender is None
or sender.id != message.chat.id
):
return

try:
await self._get_access_storage().record_identity(
self._identity_from_user(sender)
)
except RuntimeError:
logger.debug("Telegram identity storage is unavailable; skipping label")
except Exception:
logger.exception("Telegram identity recording failed; continuing update")

async def _grant_legacy_access_if_applicable(
self,
message: Message,
Expand Down
55 changes: 53 additions & 2 deletions tests/test_telegram_access.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,17 @@ def _callback_update(*, message: dict[str, object] | None) -> Update:
)


def _bot(storage: TelegramAccessStorage, *, has_history: bool) -> TelegramBot:
def _bot(
storage: TelegramAccessStorage,
*,
has_history: bool,
access_code: str | None = "test-access-code",
) -> TelegramBot:
config = TelegramConfig.model_validate(
{
"TELEGRAM_ENABLED": True,
"TELEGRAM_BOT_TOKEN": "test-token-123",
"TELEGRAM_ACCESS_CODE": "test-access-code",
"TELEGRAM_ACCESS_CODE": access_code,
}
)
bot = TelegramBot(config, _Runtime(has_history), access_storage=storage) # type: ignore[arg-type]
Expand Down Expand Up @@ -181,6 +186,52 @@ async def test_historical_private_chat_is_grandfathered(
assert await storage.is_authorized(42, "rotated-code") is True


@pytest.mark.asyncio
async def test_unconfigured_access_control_still_records_identity(
storage: TelegramAccessStorage,
) -> None:
"""Legacy open mode must still populate dashboard identity labels."""
bot = _bot(storage, has_history=False, access_code=None)

assert await bot._authorize_update(_update("normal message")) is True

row = await storage._fetch_one(
"""
SELECT display_name, username
FROM telegram_identities
WHERE telegram_user_id = ?
""",
(42,),
)
assert row == {"display_name": "Ada", "username": "ada"}


@pytest.mark.asyncio
async def test_open_mode_continues_when_identity_storage_is_unavailable(
storage: TelegramAccessStorage,
) -> None:
"""An unavailable optional label store must not block open-mode traffic."""
bot = _bot(storage, has_history=False, access_code=None)
bot.access_storage = None

with patch(
"blacki.telegram.bot.get_telegram_access_storage",
side_effect=RuntimeError("storage is not initialized"),
):
assert await bot._authorize_update(_update("normal message")) is True


@pytest.mark.asyncio
async def test_open_mode_continues_when_identity_write_fails(
storage: TelegramAccessStorage,
) -> None:
"""A local identity write failure must not block the Telegram update."""
await storage._conn.close()
bot = _bot(storage, has_history=False, access_code=None)

assert await bot._authorize_update(_update("normal message")) is True


@pytest.mark.asyncio
async def test_rotated_passphrase_user_is_not_regrandfathered(
storage: TelegramAccessStorage,
Expand Down
Loading