Skip to content

Implement the FileToLink improvement plan (H1-H10, M1-M14, L1-L10) + review hardening - #50

Open
fyaz05 wants to merge 31 commits into
mainfrom
feat/improvement-plan
Open

Implement the FileToLink improvement plan (H1-H10, M1-M14, L1-L10) + review hardening#50
fyaz05 wants to merge 31 commits into
mainfrom
feat/improvement-plan

Conversation

@fyaz05

@fyaz05 fyaz05 commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Full implementation of the improvement plan (H1-H10, M1-M14, L1-L10), hardened by an independent review cycle: 1 critical + 12 major findings, minors and leanness items — all fixed in 17 follow-up commits. Comments kept lean branch-wide.

What changed

  • Correctness: tz-aware token datetimes, TTL index conflict handling + paged backfill, self-heal deletes only on true absence (transport errors → 503 + Retry-After), fail-closed gates (unknown preflight gate, anonymous senders), RPS breaker on the immediate path, boot exit codes, bounded FloodWait waits, real config.env.local precedence
  • Security: hash-pinned Docker install (uv export + --require-hashes) with CI requirements↔lock sync gates, single-pass access-log redaction + log-forging escape, scheme allowlist in self-update, activation-redirect hardening
  • Infra: Makefile tabs, CI integration tier (testcontainers Mongo), workflow concurrency + timeouts, hermetic unit tier
  • Hygiene: ~200 LOC dead code removed; branch-wide lean comments

Verification

159 unit + 3 integration tests (real Mongo), ruff, mypy (check_untyped_defs), bandit, vulture, pip-audit, uv lock --check — green locally and in CI on every push.

…H1, H3, L8)

H1 — security dependency pass:
- pyproject.toml declares 8 exact-pinned runtime deps (was 10 unpinned)
- aiohttp 3.14.3 (OSV-clean; the 3 Jan-2026 CVEs), uvloop 0.22.1 (py3.13),
  tgcrypto -> tgcrypto-pyrofork 1.2.8 (maintained crypto backend)
- requirements.txt kept as a generated export for the Docker path
- pip-audit verified clean locally; enforced in CI

H3 — quality.yml gates (replaces docker-only CI; docker build now runs
after quality passes):
- ruff check + format, mypy (permissive baseline), pytest unit tier,
  pip-audit, bandit -ll, vulture dead-code gate (whitelist.py),
  dependency-count gate (>8 direct deps fails the build)
- .pre-commit-config.yaml mirrors the local workflow

L8 — repo hygiene pack:
- Dockerfile: non-root USER, HEALTHCHECK -> /health, git kept for H9 updates
- Makefile (format/lint/test/audit/run), .dockerignore (image no longer
  ships .git/README/tests), dependabot.yml (pip + github-actions)
- SECURITY.md, CONTRIBUTING.md
- AGENTS.md rewritten: new conventions (tg_call, preflight chain, HTML
  escaping, budgets), command table generated from bot/registry.py
- config_sample.env: every new env var annotated (PRIVATE_MODE,
  ENABLE_SHELL, ENABLE_LEGACY_LINKS, FILE_TTL_DAYS, worker pools,
  MAX_CONCURRENT_STREAMS, touch buffer, LOG_LEVEL/LOG_FORMAT, ...)
The boot path interpolated UPSTREAM_REPO/UPSTREAM_BRANCH into a
shell=True command chain and ran 'rm -rf .git', 'git reset --hard' and
mutated global git config — a live injection vector at every container
boot, and a failed update could leave a half-wiped tree.

- argv-list subprocess (shell=False); remote URL stays an argv element
- 'git pull --ff-only' replaces the init/commit/reset dance
- strictly best-effort: failures log and keep the current code running
- no-ops cleanly when git is absent or the tree is not a repo
- thunder.sh reduced to boot orchestration (update failure no longer
  blocks boot); /restart marker flow unchanged
…4, L1-L7, L9-L10)

P2 — core hygiene (H4, H5, H10):
- utils/safe_call.py: tg_call helper + reply/send/edit/delete/answer_safe
  wrappers; all ~90 inline try/except-FloodWait pairs migrated (budget:
  custom_dl streaming loop + canonical ingest loop only, as planned)
- dead code purged: dl.html + its render branch, tokens.list_tokens/
  get_user, bot_utils.notify_ch, rate_limiter.get_queue_status,
  file_properties.get_fids, canonical_files.touch_file_record +
  get_file_by_message_id (+ db twin), on_fallback_message stream path,
  Var.MULTI_CLIENT, MSG_STATS_ERROR, dead client_max_size
- three drifted media maps unified into utils/media_types.py (H4c)
- speedtest-cli removed (archived upstream, H5a); shortener rebuilt on
  aiohttp, cloudscraper dropped (H5b; curl_cffi optional extra)
- H10: LOG_LEVEL/LOG_FORMAT, shared redact_secrets(), access-log
  middleware with sha256-hashed file tokens, /log uploads redacted +
  capped at 45 MiB

P3 — runtime correctness (H6, H7, H8):
- rate limiter: worker pool (EXECUTOR_WORKERS), charge-at-exec (the old
  code double-charged: enqueue + executor), requeue-on-FloodWait with
  attempt counter (long FloodWaits no longer stall every user), global
  RPS token-bucket breaker (burst 2x), bounded structures + 5-min sweep,
  limiter occupancy in /stats
- flag_cache.py: TTL+LRU cache in front of ban/authorization checks,
  FAIL-CLOSED on DB errors (a Mongo outage no longer un-bans everyone),
  invalidated by admin mutators; OWNER_ID missing now refuses boot
- H8: Mongo timeoutMS=5000, tg_call RPC budgets (transfers unbounded),
  shortener/keepalive 10 s, last sync psutil call moved to a thread

P4 — robustness & UX parity (M1-M14):
- M6: vars.py validates ALL config problems together with named vars,
  config.env.local layering, bounds enforcement
- M1: bot/registry.py single command registry -> menu (owner-only
  hidden, 256-char truncation), /help section, AGENTS.md drift test
- M3: /health endpoint (zero deps); keepalive repointed off the
  GitHub-redirecting root onto /health with status checking
- M2: typed player page (video/audio/image/non-media cards) from mime
  type, CWD-independent Jinja loader, noindex/nofollow, noscript hint
- M7: user-facing surfaces are HTML with html.escape()d file names,
  titles and ban reasons; markdown escaper retired
- M8: atomic token activation (find_one_and_update conditioned on
  activated != True — exactly one concurrent /start wins) +
  /activate/{token} web route for shorteners
- M4a: broadcast on a 4-worker queue pool with Mongo cursor streaming,
  200 ms pacing, progress every 25 sends, cancel preserved
- M4b: batch on a 5-worker pool, order-preserving results, skipped vs
  failed counters, 30+2n deadline, MAX_BATCH_FILES enforced
- M5: shortener cache + singleflight, Bearer header auth, host
  validation, https-only, no redirects
- M9: MAX_CONCURRENT_STREAMS env admission cap (503 + Retry-After kept)
- M10: self-healing vault records — unresolved vault message => record
  deleted + 404 => next upload re-ingests; Content-Length verified
  against the actual vault media (stuck-at-99% fix)
- M11: callback panic-isolation guard with error-ID owner notification;
  close_panel permission check; catch-all answered (was already present)
- M12: unified preflight chain (banned -> private-mode -> token ->
  force-sub -> shortener) + PRIVATE_MODE allowlist; /start keeps the
  activation flow reachable; utils->bot inversions broken via client
  params (lazy fallback keeps compatibility)
- M13: bounded shutdown drain (30 s, work_loads-based) + aggregated
  teardown errors, non-zero exit on failure; restart-marker preserved
- M14: touch buffer capped with drop-on-overflow counter, env-tunable
  1-60 s flush, single BulkWrite per cycle

P5 — polish (L1-L7, L10):
- L1: ENABLE_LEGACY_LINKS flag (default on; off => 410) + legacy page
  cache (repeat /watch views no longer hit Telegram)
- L2: optional FILE_TTL_DAYS TTL index with backfill; expiry note in
  link messages only when enabled
- L3: /status gains DC id, inflight, touch-buffer stats, no-store
- L4: 32-hex public hash for new uploads; 20-hex links stay valid
- L5: session files chmod 0600 after startup
- L6: RFC 5987 filename* with ASCII fallback; 416 + Content-Range kept
- L7: preconnect hints, inline fallback stylesheet + noscript so the
  player page stays usable with CDNs blocked
- L10: /shell behind ENABLE_SHELL kill-switch (default off) + command log
…n tier

- conftest.py: hermetic env bootstrap (Thunder.vars validates at import)
- unit tier (pytest -m unit, asyncio_mode=auto, no network/Mongo):
  human_readable, time_format, media_types unification, safe_call
  FloodWait/timeout semantics, flag_cache TTL/LRU/fail paths, rate
  limiter window math + breaker + sweep, canonical hash dual-length +
  merge precedence, stream_routes parsing/range/disposition, shortener
  registry + host validation, secret redaction, config validation
  (subprocess: all problems reported together, OWNER_ID boot-fail),
  registry menu/help/AGENTS.md drift checks
- coverage gate --cov-fail-under=35 as the starting bar (raise to 80 on
  core modules next)
- tests/integration: testcontainers MongoDB (ingest-claim + atomic token
  activation), TEST_INTEGRATION=1 gated, skipped cleanly without Docker
  (mirrors ThunderGo's build-tag-gated tier, but opt-in runnable in CI)
…7 -> 7 blocks

Remaining 7 inline FloodWait references are the deliberate ones:
- safe_call.py (the helper itself, 2)
- custom_dl.py streaming pool loops (2, explicitly allowed by plan)
- rate_limiter.py requeue-on-FloodWait executor logic (1)
- canonical_files.py re-raise into the ingest retry loop (1)
- broadcast.py classification of exhausted FloodWait as failed send (1)
Comment thread .github/workflows/quality.yml Fixed
Comment thread .github/workflows/quality.yml Fixed
Comment thread Thunder/utils/shortener.py Fixed
Comment thread Thunder/server/stream_routes.py Fixed
Real bugs found by the recheck pass:
- shortener: allow_redirects=False on ClientSession() raises TypeError at
  runtime (aiohttp rejects it there) and silently disabled the M5
  shortener; redirects now enforced per-request on all 4 plugin calls
- database: missing await on AsyncCollection.aggregate() broke the
  duplicate-user dedup path (async-for over a coroutine raises TypeError)

Audit gaps closed (plan fidelity):
- H1: commit uv.lock (hash-pinned, 70 pkgs, uv sync --frozen verified on
  py3.13.15) + pip-audit in dev group + uv lock --check CI gate
- H4b/L8 hygiene: remove committed runtime log Thunder/logs/bot.txt,
  gitignore Thunder/logs/ (+ fix log.text -> log.txt)
- L8: __version__ now injectable via APP_VERSION env (default 2.2.0,
  aligned with pyproject; /status reads it)
- quality.yml: docker publish restricted to refs/heads/main so feature
  pushes can never overwrite fyaz05/thunder:latest
- mypy made BLOCKING: all 44 errors fixed (2 real bugs above + typed
  dict/float annotations + defensive list-narrowing + targeted
  stub-union ignores with comments); Makefile lint no longer || true
- 45 tracked files restored to 644 (stray 755 mode bits)

Gates on final tree: ruff check+format PASS, mypy 0 errors (35 files),
103 unit tests PASS (py3.13.15, locked deps), vulture PASS, bandit
medium+ = 0, pip-audit clean, dep count = 8, uv lock --check PASS
CRITICAL
- rate_limiter: deferred requeue busy-spun with ZERO yield points
  (Event.wait fast-path + uncontended Lock on py3.13) -> whole event
  loop froze up to 300s; now parks the pool (event clear + call_later
  timer), re-wakes on enqueue, timer cancelled at shutdown
- quality.yml: invalid YAML (unquoted ':' in step name) killed every
  gate; Makefile recipes had spaces not tabs (all targets dead)

HIGH
- callbacks: close_panel compared clicker to the BOT's id -> every
  non-owner Close button rejected; now allows panel reply target /
  private-chat peer (+ owner)
- ban gate was fail-OPEN: is_user_banned swallowed Mongo outages into
  None (negative-cached 5 min) -> raise_on_error=True for the flag
  loader; ban/unban now invalidate the flag cache
- HTML injection (M7 gaps): file_name in link messages, first_name in
  new-user log, chat_title/source_info in 5 stream.py sites escaped
- custom_dl: mid-stream FloodWait restarted from the original offset,
  re-sending delivered bytes (corrupted downloads); now resumes at
  chunk_offset + chunks_done; get_message via bounded tg_call
- stream_routes: Range end >= file_size now clamps per RFC 7233
  (was a hard 416, breaking resume clients)
- integration tier was dead twice over: wrong testcontainers import +
  missing dep + unit cov threshold; testcontainers[mongodb] declared,
  cov flags moved out of addopts
- access-log redaction hashed m.group(0)[:-len(m.group(0))] == ""
  (constant pseudonym for every file)
- CI now installs the hash-pinned uv.lock (uv sync --frozen) and
  audits the locked env incl. transitives; permissions hardened

MEDIUM
- touches added during an in-flight flush are re-armed; failed
  bulk-flush merges back instead of dropping increments
- canonical validation RPC errors keep the cached record instead of
  re-copying into BIN (vault churn)
- broadcast: strong task refs, return_exceptions, finally-cleanup of
  workers/status/registry; completion only on success
- shortener: init lock (session leak), singleflight resolves waiters
  on cancellation, Bitly host validation, session closed at shutdown
- requeued requests no longer re-charge the user window; breaker
  token consumed only at exec; cancelled workers release counts
- flusher lifecycle, force_channel negative-cache, token consume
  expiry guard, tokens -> aware UTC, update.py missing return +
  credential redaction + orphaned config backup recovery, startup
  teardown order (touch drain before db.close), umask 077, PRIVATE_MODE
  x TOKEN_ENABLED fail-fast, preflight unknown-gate warning, /log
  off-loop, /listauth batched, curl_cffi floor 0.15.0 (CVE-2026-33752)

LEANNESS
- dead _cache_by_message_id map + get_file_by_message_id removed;
  Var.LOG_LEVEL/LOG_FORMAT dead attrs removed; dead skip param and
  RPCError branch removed; help panel reuses build_help_text; README
  /speedtest residue removed; dep-count gate added to make audit

Tests: 107 passed (+4 regression: Range clamp, singleflight, park x2);
ruff/mypy/vulture/bandit/pip-audit/uv-lock gates all green.
- shortener: replace substring host matching with parsed-hostname
  exact/suffix match (_host_matches) across all 4 HTTP plugins;
  lookalikes like 'bitly.com.evil.com' or 'evil.com/bitly.com' no
  longer select a provider plugin (CodeQL high:
  py/incomplete-url-substring-sanitization)
- /activate: strict shape check (_is_activation_token) on the token
  before building the t.me redirect -- tokens are token_urlsafe(32),
  43 URL-safe chars; malformed input now 400s without a DB roundtrip
  (CodeQL medium: py/url-redirection)
- tests: +21 regression tests (host matching incl. adversarial
  lookalikes, activation token shape incl. injection payloads)
CodeQL py/url-redirection still tracked the shape-validated token into
the Location header; build the deep link via quote_plus(safe='') so the
token can only occupy the query-value slot. No-op for valid tokens
(already URL-safe); +2 tests.
Comment thread Thunder/server/stream_routes.py Fixed
CodeQL py/url-redirection models only '+ right-operand' as a
sanitizer (f-strings and quote_plus are not barriers -- verified
against UrlRedirectCustomizations.qll on github/codeql@main).  Build
the deep link so the constant 'https://t.me/' prefix is provably
outside user control; output is byte-identical for valid tokens.
Behavioral (verified by repro/code-reading):
- caption commands: /link & /dc parsed msg.text (None for captions
  under pyrofork's caption-matching filter) -> AttributeError dead air;
  now text-or-caption
- channel-ban gate: per-post DB find_one, no cache, no invalidation;
  now flag-cached with add/remove invalidation (fail-open kept
  deliberately: leave_chat on a Mongo outage would be destructive)
- legacy route error masking: get_file_info swallowed every exception
  into an unread 'error' key -> transient outages surfaced as 404;
  now propagates (route ladder maps 404/500 correctly)
- markup injection (pyrofork DEFAULT = markdown-then-HTML, so
  html.escape alone is insufficient): /dc templates converted to
  HTML + escaped + ParseMode.HTML pinned; force-sub prompt escaped;
  ban reason escaped; file-request sends pin ParseMode.HTML
- /restart: os.execv skipped the M13 touch-buffer drain, losing view
  counts; now drains before execv
- access-log middleware logged nothing for non-HTTP 500s (response
  unbound NameError swallowed); response now initialized to None
- integration fixture: env+reload never rebound the frozen Var/db
  copies -> tier could not pass; now binds Database directly and
  rebinds module handles, restoring state after
- Docker HEALTHCHECK: naive PORT parse broke on inline comments in
  the shipped sample config; now comment-stripping
- GLOBAL_RPS_LIMIT dead without GLOBAL_RATE_LIMIT: boot warning (M6)
- token_col: compound (user_id, activated, expires_at) index added
- MSG_TOKEN_FAILED: error_id kwarg was silently dropped (no slot)
- update.py: rejects leading-dash UPSTREAM_REPO/BRANCH (argv hygiene)
- _validate_short_url: trailing-dot FQDN now tolerated (parity with
  _host_matches); import_plugins glob sorted; get_user deduped;
  force-channel button reuses cached get_force_info; render_template
  reuses quote_media_name; breaker comment states real invariant

Leanness (dead code, vulture-verified):
- deleted whitelist.py (entirely stale; vulture passes without),
  ShortenerError, module-level request_executor, duplicate
  status_options handler, _REDACT_SEGMENTS, production-dead mime_for

Infra:
- Dockerfile: drop build-essential/libssl-dev (all wheels, ~150MB)
- image tags: + SHA tag for rollback; .dockerignore: cache dirs
- pre-commit ruff rev v0.8.4 -> v0.16.6 (matches lock); Makefile
  audit now audits the locked env (CI parity); dependabot uv.lock
  note; README: dead speedtest section removed, docker run now
  mounts config.env (documented flow was broken)

Gates: ruff/format clean, mypy 0 (35 files), 130 tests PASS,
vulture (whitelist-free) clean, bandit clean, uv lock --check PASS
…rmed dev extra

- C-1: AsyncMongoClient now uses tz_aware=True; pymongo's default returns
  naive UTC datetimes, and comparing them against datetime.now(UTC) in
  tokens.consume() raised TypeError on EVERY real activation -- the
  flagship M8 /start <token> path was dead at runtime
- lost CAS races now distinguish 'already' (winner activated) from
  'invalid' (expired / missing expires_at) instead of always saying 'already'
- pyproject: 'testcontainersongodb]>=4.13' -> 'testcontainers[mongodb]>=4.13'
  (invalid PEP 508; fresh 'uv lock' hard-failed on it)
- generate() no longer returns '' on an unreachable path (honest raise)
…d logs

Self-heal safety:
- transient Telegram failures (FloodWait exhaustion, timeout, transport)
  now raise TelegramUnavailable -> 503 + Retry-After; they no longer
  masquerade as FileNotFound, which made a brownout DELETE every vault
  record requested during the outage
- self-heal replacement preserves the existing public_hash (rewriting a
  legacy 20-char hash broke the 'legacy links stay valid forever' contract)
- stream_file fetches its target once (no re-fetch per FloodWait retry)
  and bounds total FloodWait sleep (60s) instead of pinning the handler
- safe_call caps a single FloodWait sleep at 30s (H8 budget honesty)

Gates / access control (fail-closed):
- preflight: unknown gate ids REJECT (were warn-and-continue = skipped
  security check); gate presets GATES_STANDARD/START/INFO added and
  common.py now routes through them (was hand-rolled per handler)
- PRIVATE_MODE denies unattributable senders (from_user None); channel
  handler refuses to mint public links in PRIVATE_MODE
- token deep-link uses quote_plus like the hardened /activate builder

Rate limiter:
- the immediate path now consumes a breaker token; a dry bucket falls
  through to the queue (bursts are shaped, not shed) -- the old top-shed
  also claimed queue-full, misleading users
- breaker gate unified on breaker.rate > 0 (GLOBAL_RPS_LIMIT-only setups)
- removed write-only user_queue_counts (+ its cancel-path leak edge)
- requeue-vs-rotation insertion policy documented

Boot / shutdown:
- boot failures exit 1 (restart policies can actually fire)
- background tasks tracked in one list (no locals().get bookkeeping);
  index-ensure task cancelled+awaited at shutdown; batched 10s wait
- deprecated get_event_loop().time() -> get_running_loop()

Config:
- config.env.local really overrides config.env now (load_dotenv
  override=False had inverted the documented precedence)
- str_to_int_set surfaces junk tokens (collect-all-errors)

Logging / misc:
- access log redacts legacy id-first capability hashes and /activate
  tokens; control chars escaped (log forging via %0A neutralized)
- redact_secrets covers API_HASH, session strings, ?start= activation tokens
- MSG_SHELL_OUTPUT_CAPTION fixes a format() KeyError that swallowed large
  /shell outputs; render_page no longer logs the capability hash
- shortener URLs html-escaped into the HTML links message
- update.py allows only https/http/git/ssh schemes (blocks ext::)
- leanness: dead touch_file_record, requested_action, task_done, peek/
  occupancy, unreachable branches, dead getattr defaults, duplicate disk
  syscalls, legacy-cache unique_id all removed
…+ integration tier

- Makefile recipes indented with hard TABs (spaces made every target fail
  with 'missing separator')
- Dockerfile installs requirements.lock (uv export of uv.lock,
  --require-hashes): transitives were previously resolved fresh and
  unpinned on every build, so CI's supply-chain guarantees never reached
  the shipped image; git dropped from the image (self-update no-ops
  without .git by design -- replace the image instead)
- CI: push trigger 'branches: ain]' corruption fixed to [main]
- CI: requirements.txt<->pyproject and requirements.lock<->uv.lock sync
  gates (two sources of truth must agree)
- CI: integration tier (real MongoDB via testcontainers) now actually
  runs -- the atomicity guarantees were never executed in CI
- CI: concurrency group + job timeout-minutes
- README: SLEEP_THRESHOLD 600 / GLOBAL_RATE_LIMIT False (vars.py ground
  truth); auto-update note (needs a git checkout)
- AGENTS.md documents the requirements.lock flow
…gressions

- test_preflight: preset ids resolve in the registry, documented order,
  unknown gate id rejects fail-closed, None-vs-False return contract
- test_config_env_layers: config.env.local overrides config.env; real
  environment still wins (subprocess, hermetic)
- test_canonical_files: replacement merge preserves legacy public_hash
- test_custom_dl_exceptions: transport errors -> TelegramUnavailable,
  genuine absence -> FileNotFound (self-heal safety contract)
- updated flag-cache and int-set tests to the leaner/fail-closed behavior
@fyaz05

fyaz05 commented Sep 6, 2026

Copy link
Copy Markdown
Owner Author

All audit findings fixed and green: 1 critical (tz-naive token activation crash), 12 majors (self-heal record deletion, fail-open gates, config.env.local precedence, Makefile/Docker/CI, TTL backfill, breaker bypass, log redaction, boot exit codes), plus minors and the leanness sweep — 88815ff..5a2cc09.

State: 159 unit + 3 integration tests, ruff, mypy, bandit, pip-audit all green in CI on head 5a2cc09.

Comment thread Thunder/server/stream_routes.py Fixed
Comment thread Thunder/server/stream_routes.py Fixed
…ntegration tests

The previous fix passed timeoutMS=120_000 to update_many, which pymongo
4.18 does not accept (no per-op CSOT override on CRUD) -- every boot with
FILE_TTL_DAYS>0 raised TypeError, aborted ensure_indexes and silently
skipped all unique indexes: the exact failure mode the fix was meant to
remove, masked in CI because tests only exercised the TTL=0 path.

Replace the single COLLSCAN update_many with an _id-paged micro-batch
migration (every statement an index seek inside the client-wide 5s
budget), bounded per boot and resumable via a migration marker; scope
the TTL conflict handler to IndexOptionsConflict (code 85) and treat
ExecutionTimeout as 'build continues server-side' instead of dropping a
live build. Add integration coverage for the full TTL lifecycle (first
boot backfill, TTL-change recreate with uniques intact, steady state,
and real row expiry).
…ogs, add route-ladder regression tests

- AppRunner(access_log=None): aiohttp's own access logger still emitted
  raw %r request lines on a latent sink (inert today only because the
  logger has no handlers; any future basicConfig() would have leaked
  unredacted paths).
- canonical stream warnings no longer log the capability hash alongside
  record/vault sizes; /log would have handed out a link fingerprint.
- unit tests pin the delivery ladder: TelegramUnavailable -> 503 with
  Retry-After and NO record deletion; true absence / media-less vault
  message -> self-heal + 404; admission slot released on every path.
Leftovers the fix session missed, all verified unreachable or write-only:
- config_parser try/except that cannot raise (digit filter + or 0)
- registry Command.hidden speculative knob ('none today')
- stream.py batch worker: orphan task_done() (no queue.join exists) and
  the dead getattr(Var, 'BATCH_WORKERS', 5) fallback
- decorators.py: unreachable empty-token guard (generate() raises, never
  returns empty)
- shortener.py: four dead getattr(Var) fallbacks (vars.py validates all)
- messages.py: unused MSG_SHELL_OUTPUT constant
- tests: immediate-path breaker consumption + dry-bucket fall-through,
  consume() status ladder incl. corrupt token without expires_at
…tency, doc truth

- vars.py honours THUNDER_SKIP_CONFIG_FILES=1; conftest sets it so a
  developer's own config.env cannot leak optional knobs into the unit
  tier; the subprocess precedence tests opt back out explicitly
- CI ruff scope now covers tests/ (was Thunder/ update.py only)
- Makefile: all targets through 'uv run' (was mixed ambient/project
  envs), find-based clean (globstar is not POSIX sh)
- requirements.txt header: the Docker image consumes requirements.lock;
  this file is the direct-dependency surface kept in sync by CI
- README: document config.env.local precedence (FAQ claimed
  'config.env Only')
The four sequential re.sub rules let the id-first rule re-match the
8-hex pseudonyms produced by the canonical rule (~39% of them end in
two digits), double-hashing the same file and stamping a misleading
"…" truncation marker on canonical /f/ links. One combined alternation
pass pins exactly-once semantics; 10 unit tests cover every family
(canon 20/32, watch, id-first, activate), the digit-tailed-pseudonym
regression, and the control-char escape.
Untyped function bodies were previously unchecked; enabling the flag
surfaces 5 real issues, all fixed at the annotation level:
- process_single(status_msg) accepts None from the batch worker, where
  no per-file status message exists (existing call sites already
  None-guard) -> Message | None
- get_readable_time() already int()-coerces internally and a test feeds
  it 90.9 -> accept int | float (the /status uptime caller passes float)
- pyrogram exposes Client.username dynamically; read via getattr with
  fallback in /status, pin via explicit ignore at boot
Fail-closed gates reject messages with no attributable from_user
(channel posts, anonymous admins); say so in the README token section
and the AGENTS gate-chain notes.
Every comment in the branch diff trimmed to 0-2 lines carrying only the
why/invariant/pitfall (plan tags H/M/L kept). No code changes: verified
by AST comparison (docstrings stripped) on all 47 touched .py files;
Makefile tabs, TOML/YAML parse, and the full local gate suite unaffected.
@fyaz05 fyaz05 changed the title Implement the FileToLink improvement plan (H1-H10, M1-M14, L1-L10) Implement the FileToLink improvement plan (H1-H10, M1-M14, L1-L10) + review hardening Sep 6, 2026
SLEEP_THRESHOLD=600 made pyrofork auto-sleep every FloodWait <= 600s
INSIDE the RPC, so tg_call's 30s wait_for budget turned recoverable
FloodWaits into TimeoutErrors and its except FloodWait branch was dead
for all realistic waits. Default now 10s (pyrofork's own default), kept
well below TG_RPC_TIMEOUT_SECONDS so auto-slept waits complete inside
the budget; longer waits surface to tg_call's bounded retry. config
sample comment corrected (threshold, not sleep time).

Also on this path:
- client.start / StreamBot.start: explicit 90s budget (session bootstrap
  outlives the lightweight-RPC budget); dropped the sleep(2) stagger that
  applied to exactly one client
- boot: executor tasks registered before the web-boot try so a later
  boot failure cancels live workers (was: registered after site.start(),
  escaping the cancellation path)
- rate_limiter: requeues always append (front-insertion was vestigial:
  every caller passes delay>0, and _process_one rotates deferred items
  to the back anyway); MAX_REQUEST_ATTEMPTS comment corrected (breaker/
  user-limit requeues are not counted by it)
- notify_own: dropped the dead try/except around gather(return_exceptions)
  and the stale send_with_flood_wait name
- flag_cache.sweep: sync (it never awaited anything)
The idfirst rule required a trailing slash, but the catch-all route
serves the no-filename legacy URL (/AbCdEf12345): its bare 6-char hash +
message id is the entire link credential, and it reached the access log
(and /log uploads, which have no 6-char-hash pattern) unredacted --
defeating the hash-fingerprinting guarantee 78c02e5 established for
canonical stream warnings. Anchor now (?=/|$); regression tests cover
the bare hash-first and id-only segments.
…eltas

- backfill: pages now filter on last_seen_at {$exists: false} and the
  resume cursor is persisted per batch. Before: last_id was local, so a
  vault >= 50k docs rescanned the first 50k every boot and never reached
  the rest (their TTL window stayed 'forever'); an exact-multiple page
  never triggered the done-marker. Convergence is pinned by unit tests
  with a fake collection (50.6k docs across two boots).
- _create_file_ttl_index: the post-drop-85 recreate also catches
  OperationFailure (a concurrent recreate must not abort the remaining
  unique ensures, per the method's own contract)
- ensure_indexes: unique/plain ensures go through a best-effort helper
  (ExecutionTimeout on a large index build aborted every ensure after it)
- touch buffer: pending entries carry (reuse_delta, seen_delta) so N
  touches of one hash flush as N increments instead of a merged $inc: 1
  (seen_count was silently undercounting /status and admin metrics)
- tokens: allowed() delegates to db.is_user_authorized instead of a
  verbatim duplicate; unreachable try/excepts around cursor-returning
  find() calls removed; ban-cache invalidation made unconditional on
  both user and channel paths
- canonical record lookup: raise_on_error=True again (the =False call
  made a Mongo transport failure indistinguishable from absence), with
  a _fetch_file_record wrapper mapping failures to 503 + Retry-After --
  brownouts used to return a cacheable 404, breaching the 'transport
  errors -> 503' self-heal contract (regression test included)
- the 503 body no longer echoes str(e): TelegramUnavailable messages
  embed the internal vault message id (CodeQL info-exposure sliver);
  constant text, detail stays in the server log
- the two delivery routes' identical inner+outer ladders (~60 lines x2)
  extracted into _admission_ladder/_route_ladder context managers;
  preview routes share the outer ladder
- /status: dropped the 'inflight' field duplicating total_workload
- custom_dl: get_file_info_sync raises FileNotFound for media-less
  messages (the {'error': ...} marker had no reader anywhere)
- parse_range_header: documented the deliberate 400-vs-RFC-ignore
  choice for structurally-broken ranges
…d code

- cancel_broadcast: owner_only guard like its sibling restart action
  (defense-in-depth: the button lives on the owner's /broadcast reply)
- close_panel: delete_safe instead of raw message.delete()
- batch summary: skipped files now counted (processed+failed < total
  with no explanation read as data loss to users)
- broadcast: _PERMANENT_ERROR_REASONS.get(type(e), default) -- except
  matches subclasses, the exact-type dict lookup raised KeyError and
  killed the worker
- logger: module-level hashlib import (per-log-line hot path)
- commands: module-level import (no circular-avoidance justification,
  unlike its siblings); config_parser: dead config_file param removed;
  preflight test: pinned knob so the assertion is not a tautology
…ments

curl_cffi was documented in pyproject, the shortener docstring and
AGENTS.md as a Cloudflare escape hatch, but no code path ever imported
it -- installing .[shortener-cf] pulled a large binary dependency that
nothing used (leanness: deleting beats implementing a speculative
fallback). uv.lock re-resolved: curl-cffi + cffi + pycparser removed.

Floors/comments corrected while here:
- testcontainers[mongodb] >= 4.15 (tests import
  testcontainers.community.mongodb, which only exists from 4.15; on
  4.13/4.14 the ImportError guard silently skipped the whole tier)
- pytest >= 8.4 (pytest-asyncio 1.4 requires it; lock already had 9.x)
- aiohttp comment: the 3 fixed CVEs are from the Aug-2026 advisory
  cycle, not Jan-2026 (that release was 3.13.3 with different CVEs)
- pyrofork pin annotated: upstream quiet since 2025-12 (drift risk)
- docker job: runs on PRs now (build only, no login, no push -- no
  secrets) and still publishes only from main pushes; before, the
  hardened Dockerfile was never exercised pre-merge, so regressions
  merged green and broke the post-merge publish. Added timeout-minutes
- actions pinned to full commit SHAs (tag-pinned uses were the last
  mutable injection vector for the docker job holding DOCKER_TOKEN);
  dependabot's github-actions ecosystem keeps them current
- uv pinned in both jobs: lock/export output format and gate semantics
  must not drift between commits
- bandit: -c pyproject.toml (the [tool.bandit] section was inert without
  it); mypy/vulture/bandit/ruff scope now includes update.py (the
  security-sensitive self-update module was only ruff-linted)
- Makefile: make lock (the three CI drift gates, previously
  local-green/CI-red), make integration, parity with the widened scopes
- Dockerfile: dropped 'pip install --upgrade pip' -- the sole unhashed
  install; 3.13-slim's pip fully supports --require-hashes
- CONTRIBUTING: uv sync --frozen setup (the old venv/pip instructions
  never installed uv, which every Makefile recipe requires), pre-commit
  wiring, make lock/integration documented
- .dockerignore: uv.lock/requirements.txt/AGENTS.md excluded from the
  image (dead weight; requirements.lock stays -- it is COPY'd early);
  .gitignore trailing newline
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants