Alerts: a Telegram channel, one chat or group each, connected by a one-time link

The channel sends through the Bot API with httpx only: HTML with escaped text,
cut to fit a message, silent for a warning, a short 429 waited out once, and
errors that say what to do about a blocked bot, a lost chat or a bad token and
never quote the request URL. The bot token is a stored secret: saving a
channel refuses a literal one, the card's "Store token" writes the secret and
a reference, and no endpoint returns it.

A chat id is never typed. Connect starts a ten minute code, and the chat that
sends `/start <code>` back through the bot is the one connected, so it is a
chat somebody opened a link for. The engine fetches updates only while a code
is waiting (no standing poll, no webhook, no inbound address) and ignores
everything else without a reply. Connect needs a saved form, and the form
takes the connected chat in without touching what was typed since.

The token is part of every Bot API URL, so the httpx logger gets the same
filter the request lines have, which now also masks an httpx.URL argument.
This commit is contained in:
2026-10-28 13:06:58 +01:00
parent bfaaa7cd03
commit 5d0ea480bf
14 changed files with 1462 additions and 11 deletions
+134
View File
@@ -140,3 +140,137 @@ def test_any_account_can_subscribe_its_own_browser(
assert response.status_code == 200
assert response.json()["key"]
def _telegram_config(token: Any) -> dict[str, Any]:
return {
"channels": [
{
"name": "garage",
"kind": "telegram",
"config": {"token": token} if token else {},
}
],
"rules": [],
}
def test_a_bot_token_has_to_be_a_stored_secret(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
plain = client.put(
f"{PREFIX}/config",
headers=superuser_token_headers,
json=_telegram_config("123456:literal-token"),
)
stored = client.put(
f"{PREFIX}/config",
headers=superuser_token_headers,
json=_telegram_config({"$secret": "telegram-bot"}),
)
assert plain.status_code == 422
assert "stored secret" in plain.json()["detail"]
assert stored.status_code == 200
def test_a_chat_connects_through_the_link_and_the_token_is_never_returned(
client: TestClient,
superuser_token_headers: dict[str, str],
normal_user_token_headers: dict[str, str],
monkeypatch: pytest.MonkeyPatch,
) -> None:
import json
import time
import httpx
from fluksio.flow import telegram
token = "123456:SECRET-token"
client.put(
f"{settings.API_V1_STR}/secrets/telegram-bot",
headers=superuser_token_headers,
json={"value": token},
)
client.put(
f"{PREFIX}/config",
headers=superuser_token_headers,
json=_telegram_config({"$secret": "telegram-bot"}),
)
def bot(request: httpx.Request) -> httpx.Response:
method = request.url.path.rsplit("/", 1)[1]
assert request.url.path.startswith(f"/bot{token}/")
result: Any = True
if method == "getMe":
result = {"username": "fluksio_bot"}
elif method == "getUpdates":
time.sleep(0.05)
codes = [c for c, p in telegram.linker._pending.items() if p.token == token]
result = (
[
{
"update_id": 1,
"message": {
"text": f"/start {codes[0]}",
"chat": {"id": 42, "first_name": "Anna"},
},
}
]
if codes
else []
)
return httpx.Response(200, content=json.dumps({"ok": True, "result": result}))
transport = httpx.MockTransport(bot)
monkeypatch.setattr(
telegram,
"_client",
lambda timeout: httpx.AsyncClient(transport=transport, timeout=timeout),
)
link_url = f"{PREFIX}/telegram/garage/link"
assert client.post(link_url, headers=normal_user_token_headers).status_code == 403
response = client.post(link_url, headers=superuser_token_headers)
assert response.status_code == 200
body = response.json()
assert body["url"].startswith("https://t.me/fluksio_bot?start=")
assert body["group_url"].startswith("https://t.me/fluksio_bot?startgroup=")
assert token not in response.text
channel: dict[str, Any] = {}
for _ in range(100):
config = client.get(f"{PREFIX}/config", headers=superuser_token_headers)
channel = config.json()["channels"][0]
if channel["config"].get("chat_id"):
break
time.sleep(0.05)
assert channel["config"]["chat_id"] == "42"
assert channel["config"]["label"] == "Anna"
assert channel["config"]["token"] == {"$secret": "telegram-bot"}
assert token not in config.text
status = client.get(link_url, headers=superuser_token_headers).json()
assert status["waiting"] is False
def test_a_link_needs_a_stored_token(
client: TestClient, superuser_token_headers: dict[str, str]
) -> None:
client.put(
f"{PREFIX}/config",
headers=superuser_token_headers,
json=_telegram_config(None),
)
response = client.post(
f"{PREFIX}/telegram/garage/link", headers=superuser_token_headers
)
unknown = client.post(
f"{PREFIX}/telegram/nowhere/link", headers=superuser_token_headers
)
assert response.status_code == 422
assert unknown.status_code == 404