Commit Graph

74 Commits

Author SHA1 Message Date
agent_coder 3f4c7f0f68 refactor(render): этап 1 — sanitizer.py + санитайз per-post в пайплайне (#28)
Часть эпика рефакторинга рендер-пайплайна (#34). Один bleach-конфиг на весь
проект, один санитайз на пост, ноль thread-hop'ов в циклах (раньше конфиг был
скопирован трижды и разъехался; RSS-путь делал to_thread + новый CSSSanitizer +
вложенную функцию НА КАЖДЫЙ пост, хотя _render_pipeline уже в worker-треде).

Новый sanitizer.py — единственная bleach-конфигурация: ALLOWED_TAGS (union трёх
старых копий, вкл. s/del), ALLOWED_PROTOCOLS=['http','https','tg'],
sanitize_html() зовёт bleach.clean(..., protocols=..., strip=True) (оба не-дефолт),
fail-CLOSED (html.escape при любом исключении), единое имя лога
html_sanitization_error + log_context. _render_pipeline санитайзит каждый пост
после рендера (в своём worker-треде); rss_generator −71 строка.

Реестр §3:
- §3.1 s/del теперь в фидах (раньше выживали только в одиночном посте);
- §3.2 fail-open → fail-closed (вкл. single-post/JSON путь);
- §3.3 <hr class="post-divider"> реально виден (join ПОСЛЕ санитайза; hr не в
  whitelist, поэтому раньше вырезался strip=True);
- §3.4 несбалансированный фрагмент нормализуется в границах своего поста;
- §3.5 гранулярный fail-closed HTML-фида (падает один пост, не весь фид);
- §3.16 три имени санитайз-логов объединены в html_sanitization_error+log_context.

Golden-эталоны обновлены: КАЖДЫЙ изменённый байт привязан к пункту §3 (net-diff
тегов: только +<s>/§3.1 и +<hr>/§3.3; <div> сбалансирован +/- = §3.4;
после снятия этих токенов + нормализатора все 6 голденов побайтово old==new).
Оракул этапа 0 зелёный. Полный сюит 346 passed (+14 sanitizer-тестов).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 16:42:06 +03:00
claude code agent 8d21390294 fix(stability): fail CLOSED on feed-sanitize exception + cover bulk-SQL/flush (review round 1)
#1 [security] The final feed-sanitize try/except was FAIL-OPEN: since 4.4 removed
the per-fragment passes, post['html'] / the concatenated feed html is now raw
channel-controlled HTML, and this is its ONLY sanitize. If bleach itself throws
(RecursionError/OOM on pathological nested HTML — a class already seen in this
project), the except branch returned the RAW payload = stored XSS. Both branches
(RSS per-post, HTML whole-feed) now fail CLOSED via html.escape, so the content
survives as inert text and no live tag ever reaches the client. Added a test that
forces bleach to raise and asserts no live <script>/<img>/<a> reaches the feed
(adversarially validated: reverting to fail-open makes it fail).

#2 [test] Direct test of the new upsert_media_file_ids_bulk_sync on a temp DB:
empty no-op, multi-row insert, and re-upsert-updates-added (the real executemany +
ON CONFLICT, previously only mocked).

#3 [test] Test that media ids collected before a render exception are still
flushed (the flush is in a finally) — removing the finally now turns a test red.

#4 [cleanup] Removed the dead  in rss_generator (the
flush is delegated to post_parser._flush_pending_media_ids).

#5 [doc] Corrected the sanitize-map comments: RSS sanitizes per-post, only HTML is
the whole-feed pass.

233 passed (214 baseline + 19).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 09:41:09 +03:00
claude code agent 9f9091f6fb fix(stability): stage 4 — event-loop hygiene for feed generation
Feed generation blocked the loop (deepcopy of hundreds of Messages + per-message
bleach x4 + str(message) per post). This moves the CPU work off the loop and cuts
it down.

4.1 raw_message is lazy: process_message(include_raw=False) omits str(message)
    entirely; only /json and debug-HTML compute it.
4.2 (done BEFORE 4.3) _save_media_file_ids no longer touches asyncio/DB — it
    appends to the per-request PostParser._pending_media_ids, and the caller
    flushes once via to_thread(upsert_media_file_ids_bulk_sync) (a new executemany
    fn in file_io.py). Removed _persist_pending_count + the create_task machinery.
    This had to precede 4.3 or get_running_loop() inside the render thread would
    raise and silently kill media-id persistence.
4.3 The render pipeline (_create_time_based_media_groups, _create_messages_groups,
    _trim_messages_groups, _render_messages_groups) is now plain-sync and runs in
    ONE asyncio.to_thread(_render_pipeline) from both feed generators; deepcopy
    moved into the thread. The render path is verified free of asyncio primitives.
4.4 Sanitize is now ONE pass per output boundary (removed the 4 internal
    per-fragment bleach passes): RSS/HTML feeds sanitize once at the whole-feed
    pass; /html and /json sanitize body+footer once in process_message; the debug
    <pre> raw dump is html.escape'd. Media embeds in body, reactions in footer, so
    both are covered by the boundary pass. XSS tests (<script>/onerror=/javascript:)
    confirm all four outputs are clean.

Review round-1: documented that flags are now extracted from the pre-sanitize body
(a 4.4 consequence — non-security, legitimate links unaffected); added
media-caption XSS tests for the exact fragments whose internal passes were removed
(adversarially validated: neutering the sanitizer makes them fail); the media-id
flush is now in a finally so a partial render still persists what it collected.

230 passed (214 baseline + 16).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 09:16:14 +03:00
claude code agent 03d1de2954 fix(stability): declare pytest-asyncio, bound /raw_json RPC, assert FloodWait backoff, dedupe gate+timeout (review round 1)
F1 [WARNING] pytest-asyncio was not declared, so the 6 new async stage-1 tests
ERROR on a clean checkout (async def not natively supported) — zero regression
protection, masked by a globally-installed plugin. Added pytest-asyncio to
requirements.txt + asyncio_mode=auto to tests/pytest.ini. Verified on a fresh venv
from requirements alone: the tests collect and run (180 passed).

F2 [WARNING] The /raw_json endpoint's get_messages was the one remaining live RPC
without a timeout, violating the stage-1 DoD ('every Telegram RPC is bounded').
Wrapped it in asyncio.wait_for(..., 30) mirroring PostParser.get_post. (It is not
under the tg_rpc gate, so its blast radius was one request, not the app.)

F3 [WARNING] The worker test globally no-op'd asyncio.sleep, so the dedicated
except FloodWait branch was indistinguishable from the generic handler — deleting
it kept the test green. The sleep stub now records delays and the test asserts the
FloodWait backoff of 6 (=min(1+5,900)), distinct from the success path's 2.

F5 [low] The tricky 'gate outside, timeout inside' nesting was open-coded at 3
sites (each re-deriving the invariant). Extracted tg_rpc_bounded(timeout) into
tg_throttle (using asyncio.timeout()); the 3 sites now use it, so a future call
site cannot silently wrap the gate entry and reopen the hang-under-backpressure.

F4 [low] Documented TG_RPC_TIMEOUT in the dockercompose.yml environment block
next to the other TG_RPC_* knobs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 07:12:10 +03:00
claude code agent 4daf611f05 fix(stability): stage 1 — timeouts on all TG RPC + resilient download worker + supervision
Eliminates the app-wide hangs where one stuck Telegram RPC holds the single
tg_rpc gate permit forever (freezing every RSS/HTML request) and where the
background download worker dies silently.

1.1 Every RPC under the global gate is now bounded by asyncio.wait_for
    (config tg_rpc_timeout, env TG_RPC_TIMEOUT, default 60). The wait_for wraps
    ONLY the RPC body, never the gate acquire, so queue backpressure stays
    legitimate; the paginated get_chat_history is collected in an inner coroutine
    so the async-for can be bounded. A timeout propagates out of the gate so its
    permit is released (no leak).
1.2 The other live RPC paths get the same treatment: _reply_enrichment's
    get_messages now runs under the gate + timeout; PostParser.get_post is bounded
    at 30s (and a stray print removed); /health's get_me at 10s.
1.3 background_download_worker: queue.get() moved out of the try so task_done()
    in finally balances exactly one get (no ValueError that killed the worker);
    FloodWait is caught BEFORE the generic Exception (sleeps, does not flood);
    download_new_files uses put_nowait so a full queue no longer blocks the
    sweeper forever.
1.4 Both background tasks run under a _supervised() wrapper: a crash or an
    unexpected return is logged CRITICAL and restarted (restarts rate-limited to
    once per 60s so a hard-failing task can't spin), while CancelledError is
    propagated to the child for a clean shutdown.

Tests (tests/test_stage1_hangs.py): gate timeout releases the permit + a second
call succeeds; gate cancel mid-spacing releases the permit; the worker survives
Exception and FloodWait with balanced task_done; and _supervised restarts a
crashing task (rate-limited) and an unexpected return, and propagates cancellation
to its child. 180 passed (174 baseline + 6).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 06:51:08 +03:00
vvzvlad 080dae3e74 feat(cache): add channel info cache with TTL and throttling
Docker Image CI / build (push) Has been cancelled
Introduce `cached_get_chat` in `tg_cache` to store channel metadata on disk with a configurable TTL (default 12 hours). Use the `tg_rpc` throttling context for RPC calls. Update `rss_generator` to use the cached version and document related environment variables in `dockercompose.yml`.
2026-06-05 20:45:08 +03:00
vvzvlad f0e38b9776 perf(core): improve async handling and caching performance
Convert delayed_delete_file to async and use asyncio.sleep.
Add pre‑semaphore cache hit to serve cached files without acquiring the download semaphore.
Set SQLite busy_timeout to reduce lock errors.
Introduce a diagnostics counter for pending _persist_media_file_id_async tasks.
Refactor flag extraction to accept pre‑generated HTML and compute html body once.
Batch reply enrichment in RSS generator to minimize API calls.
Store cached messages directly in tg_cache and add fallback for legacy double‑pickle format.

BREAKING CHANGE: delayed_delete_file is now async and must be awaited.
2026-05-18 00:08:46 +03:00
vvzvlad ab8f15d49d feat(api): add atomic temp file handling for media downloads
- Use unique temporary file paths with UUID to avoid race conditions during concurrent downloads.
- Perform atomic rename of the temp file to the final cache location and handle existing concurrent downloads.
- Clean up zero‑size and stale temporary files, including new “.tmp.<hex>” pattern.
- Extend cleanup logic to detect and remove race‑condition temp files.
- Validate and convert environment variables (TG_API_ID, TG_PROXY_PORT, API_PORT) with clear error messages and proper exit handling.
- Flush prints and replace `os._exit` with `sys.exit` for graceful termination.
- Fix URL regex in `PostParser` and guard against missing channel usernames when generating media URLs.
- Deep‑copy message lists in RSS generator to prevent mutation across calls.
- Propagate `FloodWait` exceptions to be handled by the API server instead of retrying internally.
- Enhance `TelegramClient` disconnect handling with a brief pause, shutdown check, and reconnection reset.
- Refine auth retry logic to check for actual `KeyError` instances.
2026-05-17 23:49:18 +03:00
vvzvlad 64fe6327d8 fix(rss): handle missing dates in media grouping and RSS
Add deterministic fallback sorting for messages without a date to avoid
TypeError during grouping. Use current time as fallback when calculating
time differences and when generating RSS entries. Sort rendered posts
with epoch fallback for None dates and emit a warning when a post lacks a
date. These changes make the RSS generation robust against messages that
do not have timestamps.
2026-05-17 22:37:20 +03:00
vvzvlad 6e2c1f79d3 fix(rss): stop popping oldest group in _trim_messages_groups
Removed the pop operation that discarded the oldest message group during trimming, preventing unintended loss of messages.
2026-05-17 22:28:28 +03:00
vvzvlad 9bfef3bf2d fix(rss): remove filter for NEW_CHAT_TITLE messages
Previously NEW_CHAT_TITLE events were ignored in the RSS generator,
preventing title updates from being included. The filter line has been
removed so these messages are now processed correctly.
2026-04-16 04:02:10 +03:00
vvzvlad fef63c1224 chore(rss_generator): add logging for RSS feed date ranges and entry timestamps
Logs the date range (oldest/newest) and total count of posts being added to RSS feeds, plus debug-level logging for each entry's timestamp conversion. This improves observability for feed generation and helps diagnose date-related issues.
2026-01-01 15:02:36 +03:00
vvzvlad 337cabe180 fix(rss_generator): handle Telegram flood wait errors with automatic retry logic
Add flood wait exception handling in both RSS and HTML generation functions.
The bot now automatically waits for the specified duration and retries the
operation when encountering rate limiting from Telegram's API.
2026-01-01 14:57:16 +03:00
vvzvlad deacb8b9a0 refactor rss_generator: offload HTML concatenation and sanitization to asyncio.to_thread to improve CPU performance and prevent event loop blocking 2025-09-15 12:16:53 +03:00
vvzvlad 14081a3d44 refactor rss_generator and tg_cache: replace cached_get_chat with direct client.get_chat calls, update message caching logic to handle limits, and improve cache file naming for message history 2025-06-10 03:48:08 +03:00
vvzvlad b7a8f4f60d refactor requirements and rss_generator: replace direct get_chat calls with cached_get_chat for improved performance and update requirements.txt to uncomment Kurigram 2025-06-10 02:40:23 +03:00
vvzvlad 0999e86f2b add timing logs for RSS and HTML generation in api_server and rss_generator 2025-04-27 18:40:31 +04:00
vvzvlad b9d5e839df refactor post_parser and rss_generator:move media in body render 2025-04-26 12:38:52 +04:00
vvzvlad 5c34d62c11 EPIC: support REPLY_TO in rss 2025-04-19 04:49:01 +03:00
vvzvlad ce39431998 move import 2025-04-18 23:34:26 +03:00
vvzvlad 386e7e8ce4 improve logging 2025-04-18 23:33:47 +03:00
vvzvlad 26744e3699 refactoring 2025-04-18 23:02:29 +03:00
vvzvlad 27f39a3948 add processed_message_to_tg_message for merge flags 2025-04-18 21:08:20 +03:00
vvzvlad 3f253b7a0e add template 2025-04-18 21:05:16 +03:00
vvzvlad 8321844a3e refactoring 2025-04-18 20:57:48 +03:00
vvzvlad 1760ec3485 add types-2 2025-04-18 20:39:57 +03:00
vvzvlad 1b2cc65899 fix bug 2025-04-18 20:31:37 +03:00
vvzvlad 6aba094a6c add types support 2025-04-18 19:42:54 +03:00
vvzvlad cb419c97af update pylint and pylance disable comments 2025-04-18 18:00:02 +03:00
vvzvlad 7621235baa todo 2025-04-18 05:38:48 +03:00
vvzvlad c9d5f6b2f6 refactor PostParser and RSS generator to remove header generation and adjust body rendering 2025-04-18 05:06:26 +03:00
vvzvlad 9e394b28e2 add merged flag to processed messages in RSS generator 2025-04-18 05:02:09 +03:00
vvzvlad 5dc5783b86 Revert "Enhance RSS description generation"
This reverts commit ef5206a0c8.
2025-04-16 06:18:45 +03:00
vvzvlad ef5206a0c8 Enhance RSS description generation 2025-04-16 03:18:47 +03:00
vvzvlad 77d85ad3be Update generate_html_footer to accept optional flags_list for improved flexibility in HTML generation 2025-04-15 19:06:24 +03:00
vvzvlad 8deb96ce38 Refactor PostParser and RSS generator to improve flag handling and HTML footer generation 2025-04-15 18:56:12 +03:00
vvzvlad 1098770c53 Collect unique flags from processed messages in _render_messages_groups 2025-04-15 18:14:43 +03:00
vvzvlad 1871843b17 small refactoring 2025-04-15 02:43:26 +03:00
vvzvlad 0600ce4307 add more logging 2025-04-14 17:06:25 +03:00
vvzvlad defba4cdaf Enhance PostParser to handle service messages 2025-04-13 19:14:44 +03:00
vvzvlad 6c8debbff6 Add condition to ignore VIDEO_CHAT_STARTED messages in message grouping 2025-04-12 19:57:50 +03:00
vvzvlad 422e2adc2d Add shebang and encoding declaration to multiple Python files 2025-04-03 01:48:34 +03:00
vvzvlad 3d54230169 Update PostParser to classify unknown posts with a new emoji and enhance RSS generator to ignore new chat title events 2025-04-01 02:22:59 +03:00
vvzvlad 93698e0d4e adding UNICODE flag to regex pattern 2025-03-22 15:11:30 +03:00
vvzvlad d3fe2c1b71 exclude_text filtering now use regex 2025-03-22 14:46:36 +03:00
vvzvlad ac2c06fe22 Update limit validation in generate_channel_rss to allow a maximum of 200 instead of 100 2025-03-22 13:55:04 +03:00
vvzvlad a17a4aa860 Update media section formatting in _render_messages_groups to include line breaks for better HTML presentation 2025-03-21 15:32:20 +03:00
vvzvlad e2701d7979 Skip service messages about new chat photos 2025-03-11 13:47:51 +03:00
vvzvlad 716fa7ee42 Add text exclusion filter 2025-03-04 03:15:26 +10:00
vvzvlad 2dc1fecc66 Improve error logging and increase HTML generation limit 2025-02-10 05:54:28 +03:00