docs: document server-side rendering pipeline, response timing middleware, and Telegram pairing API

- Add comprehensive documentation for backend content rendering in AGENTS.md, detailing the new `render_content` and `render_title` Jinja globals built on mistune with media processing, emoji shortcodes, and XSS protection
- Document the `X-Response-Time` header and bottom-left render time indicator in README.md
- Update bot token pricing documentation to clarify fallback vs gateway cost headers
- Add `email_accounts` to soft-delete tables and `idx_users_role` composite index in database schema
- Implement `telegram_pairings` and `telegram_links` table creation with column migration and indexes
- Add `/profile/{username}/telegram` endpoint to docs API with request/unpair actions
- Register `TelegramService` in main.py lifespan and add `response_timing` middleware emitting `X-Response-Time` header
- Introduce `TelegramPairForm` model and `guard_public_host_sync` synchronous host validation function
This commit is contained in:
2026-06-18 22:09:34 +00:00
parent 95dca73291
commit 6ceca3d0d4
146 changed files with 6079 additions and 392 deletions
+1
View File
@@ -0,0 +1 @@
# retoor <retoor@molodetz.nl>
+53
View File
@@ -0,0 +1,53 @@
# retoor <retoor@molodetz.nl>
from devplacepy.services.telegram.format import (
markdown_to_telegram_html,
split_for_telegram,
)
def test_markdown_basic_formatting():
out = markdown_to_telegram_html("Hello **bold** and _em_ and `code`")
assert "<b>bold</b>" in out
assert "<i>em</i>" in out
assert "<code>code</code>" in out
def test_markdown_link_kept():
out = markdown_to_telegram_html("see [docs](https://example.com/x)")
assert '<a href="https://example.com/x">docs</a>' in out
def test_markdown_escapes_special_characters():
out = markdown_to_telegram_html("a < b & c > d")
assert "&lt;" in out and "&amp;" in out and "&gt;" in out
def test_markdown_drops_unsupported_tags():
out = markdown_to_telegram_html("# Heading\n\ntext")
assert "<h1>" not in out
assert "<b>Heading</b>" in out
def test_split_respects_limit():
chunks = split_for_telegram("a" * 5000, limit=3900)
assert all(len(chunk) <= 3900 for chunk in chunks)
assert "".join(chunks) == "a" * 5000
def test_split_short_text_single_chunk():
assert split_for_telegram("hello") == ["hello"]
def test_split_breaks_single_oversized_line():
chunks = split_for_telegram("b" * 9000, limit=4000)
assert all(len(chunk) <= 4000 for chunk in chunks)
assert "".join(chunks) == "b" * 9000
def test_split_empty_text_returns_single_empty_chunk():
assert split_for_telegram("") == [""]
def test_markdown_empty_input_is_empty():
assert markdown_to_telegram_html("") == ""
+78
View File
@@ -0,0 +1,78 @@
# retoor <retoor@molodetz.nl>
from datetime import datetime, timedelta, timezone
from devplacepy.database import get_table
from devplacepy.services.telegram import store
def _seed_user(uid: str) -> None:
users = get_table("users")
if not users.find_one(uid=uid):
users.insert({"uid": uid, "username": uid, "api_key": f"key-{uid}"})
def test_issue_code_is_four_digits(local_db):
_seed_user("tg-user-1")
issued = store.issue_code("tg-user-1")
assert issued["code"].isdigit() and len(issued["code"]) == 4
assert issued["ttl_minutes"] >= 1
def test_verify_binds_and_is_single_use(local_db):
_seed_user("tg-user-2")
code = store.issue_code("tg-user-2")["code"]
user = store.verify_code(code, chat_id=555, from_id=777)
assert user is not None and user["uid"] == "tg-user-2"
assert store.is_paired("tg-user-2")
assert store.user_for_chat(555)["user_uid"] == "tg-user-2"
assert store.verify_code(code, chat_id=555, from_id=777) is None
def test_reissue_invalidates_previous_code(local_db):
_seed_user("tg-user-3")
first = store.issue_code("tg-user-3")["code"]
store.issue_code("tg-user-3")
assert store.verify_code(first, chat_id=10, from_id=11) is None
def test_expired_code_rejected(local_db):
_seed_user("tg-user-4")
code = store.issue_code("tg-user-4")["code"]
pairings = get_table("telegram_pairings")
row = pairings.find_one(user_uid="tg-user-4", used=0)
past = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
pairings.update({"id": row["id"], "expires_at": past}, ["id"])
assert store.verify_code(code, chat_id=1, from_id=2) is None
def test_unpair_removes_link(local_db):
_seed_user("tg-user-5")
code = store.issue_code("tg-user-5")["code"]
store.verify_code(code, chat_id=88, from_id=99)
assert store.unpair("tg-user-5") == 1
assert not store.is_paired("tg-user-5")
def test_verify_rejects_non_four_digit_code(local_db):
assert store.verify_code("abcd", chat_id=1, from_id=2) is None
assert store.verify_code("12345", chat_id=1, from_id=2) is None
assert store.verify_code("12", chat_id=1, from_id=2) is None
def test_user_for_chat_unknown_is_none(local_db):
assert store.user_for_chat(999_999) is None
assert store.link_for_user("tg-no-such-user") is None
def test_unpair_unknown_user_removes_nothing(local_db):
assert store.unpair("tg-never-paired") == 0
def test_rebind_moves_chat_to_latest_user(local_db):
_seed_user("tg-user-6")
_seed_user("tg-user-7")
store.bind("tg-user-6", chat_id=4242, from_id=1)
store.bind("tg-user-7", chat_id=4242, from_id=1)
assert store.user_for_chat(4242)["user_uid"] == "tg-user-7"
assert not store.is_paired("tg-user-6")
+100
View File
@@ -0,0 +1,100 @@
# retoor <retoor@molodetz.nl>
import asyncio
from devplacepy.services.telegram.backend import FakeTelegramBackend
from devplacepy.services.telegram.worker import TelegramWorker
def _run(coro):
return asyncio.new_event_loop().run_until_complete(coro)
def _worker(backend):
frames = []
async def emit(frame):
frames.append(frame)
return TelegramWorker(backend, emit), frames
def test_private_text_message_emitted():
backend = FakeTelegramBackend()
worker, frames = _worker(backend)
_run(
worker.process_update(
{
"update_id": 1,
"message": {
"chat": {"id": 42, "type": "private"},
"from": {"id": 7},
"text": "hello",
},
}
)
)
messages = [f for f in frames if f["type"] == "message"]
assert len(messages) == 1
assert messages[0]["chat_id"] == 42 and messages[0]["text"] == "hello"
def test_group_message_ignored():
backend = FakeTelegramBackend()
worker, frames = _worker(backend)
_run(
worker.process_update(
{
"update_id": 1,
"message": {
"chat": {"id": 9, "type": "group"},
"from": {"id": 7},
"text": "x",
},
}
)
)
assert not [f for f in frames if f["type"] == "message"]
def test_photo_becomes_data_uri():
backend = FakeTelegramBackend()
backend.files["abc"] = b"\xff\xd8\xff" + b"0" * 50
worker, frames = _worker(backend)
_run(
worker.process_update(
{
"update_id": 1,
"message": {
"chat": {"id": 42, "type": "private"},
"from": {"id": 7},
"photo": [{"file_id": "abc", "file_size": 53}],
},
}
)
)
message = [f for f in frames if f["type"] == "message"][0]
assert message["images"][0].startswith("data:image/jpeg;base64,")
def test_html_send_falls_back_to_plain_on_entity_error():
class EntityBackend(FakeTelegramBackend):
async def send_message(self, chat_id, text, parse_mode):
if parse_mode:
return {
"ok": False,
"error_code": 400,
"description": "Bad Request: can't parse entities",
}
return await super().send_message(chat_id, text, None)
backend = EntityBackend()
worker, frames = _worker(backend)
_run(
worker.handle_command(
{"cmd": "send", "req_id": "r1", "chat_id": 42, "text": "<b>x</b>", "parse_mode": "HTML"}
)
)
assert backend.sent[-1]["parse_mode"] is None
result = [f for f in frames if f["type"] == "result"][0]
assert result["ok"] and result["req_id"] == "r1"