feat: geo location as Google Maps link in Telegram notifications

When signal has geo, show clickable Google Maps link instead of raw
coordinates. Without geo, show "Гео нету". Added parse_mode=HTML
to send_message for link rendering.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Gros Frumos 2026-03-21 14:21:41 +02:00
parent 04f7bd79e2
commit 47b89ded8d
4 changed files with 8 additions and 8 deletions

View file

@ -223,9 +223,9 @@ async def signal(
ts = datetime.fromtimestamp(body.timestamp / 1000, tz=timezone.utc) ts = datetime.fromtimestamp(body.timestamp / 1000, tz=timezone.utc)
geo_info = ( geo_info = (
f"📍 {lat}, {lon}{accuracy}м)" f"📍 <a href=\"https://maps.google.com/maps?q={lat},{lon}\">{lat}, {lon}</a>{accuracy:.0f}м)"
if geo if geo
else "Без геолокации" else "Гео нету"
) )
text = ( text = (
f"🚨 Сигнал от {user_name}\n" f"🚨 Сигнал от {user_name}\n"

View file

@ -50,7 +50,7 @@ async def send_message(text: str) -> None:
url = _TELEGRAM_API.format(token=config.BOT_TOKEN, method="sendMessage") url = _TELEGRAM_API.format(token=config.BOT_TOKEN, method="sendMessage")
async with httpx.AsyncClient(timeout=10) as client: async with httpx.AsyncClient(timeout=10) as client:
for attempt in range(3): for attempt in range(3):
resp = await client.post(url, json={"chat_id": config.CHAT_ID, "text": text}) resp = await client.post(url, json={"chat_id": config.CHAT_ID, "text": text, "parse_mode": "HTML"})
if resp.status_code == 429: if resp.status_code == 429:
retry_after = resp.json().get("parameters", {}).get("retry_after", 30) retry_after = resp.json().get("parameters", {}).get("retry_after", 30)
sleep = retry_after * (attempt + 1) sleep = retry_after * (attempt + 1)

View file

@ -119,7 +119,7 @@ async def test_signal_message_contains_registered_username():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_signal_message_without_geo_contains_bez_geolocatsii(): async def test_signal_message_without_geo_contains_bez_geolocatsii():
"""When geo is None, message must contain 'Без геолокации'.""" """When geo is None, message must contain 'Гео нету'."""
async with make_app_client() as client: async with make_app_client() as client:
api_key = await _register(client, _UUID_S3, "Bob") api_key = await _register(client, _UUID_S3, "Bob")
with patch("backend.telegram.send_message", new_callable=AsyncMock) as mock_send: with patch("backend.telegram.send_message", new_callable=AsyncMock) as mock_send:
@ -129,7 +129,7 @@ async def test_signal_message_without_geo_contains_bez_geolocatsii():
headers={"Authorization": f"Bearer {api_key}"}, headers={"Authorization": f"Bearer {api_key}"},
) )
text = mock_send.call_args[0][0] text = mock_send.call_args[0][0]
assert "Без геолокации" in text assert "Гео нету" in text
@pytest.mark.asyncio @pytest.mark.asyncio

View file

@ -140,7 +140,7 @@ async def test_signal_with_geo_send_message_contains_coordinates():
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_signal_without_geo_send_message_contains_no_geo_label(): async def test_signal_without_geo_send_message_contains_no_geo_label():
"""Criterion 1: when geo is null, Telegram message contains 'Без геолокации'.""" """Criterion 1: when geo is null, Telegram message contains 'Гео нету'."""
sent_texts: list[str] = [] sent_texts: list[str] = []
async def _capture(text: str) -> None: async def _capture(text: str) -> None:
@ -158,8 +158,8 @@ async def test_signal_without_geo_send_message_contains_no_geo_label():
await asyncio.sleep(0) await asyncio.sleep(0)
assert len(sent_texts) == 1 assert len(sent_texts) == 1
assert "Без геолокации" in sent_texts[0], ( assert "Гео нету" in sent_texts[0], (
f"Expected 'Без геолокации' in message, got: {sent_texts[0]!r}" f"Expected 'Гео нету' in message, got: {sent_texts[0]!r}"
) )