Telegram channel posts up through v1.9.9 inlined the full Persian half of `docs/changelog/v{version}.md` (often >2000 chars), with sub-bullets, contributor mentions, and architectural prose. In a chat-client viewport the result was an unreadable wall of mixed RTL Persian + LTR `<code>` / `<b>` spans + nested bullets that scrolled past most readers.
Switched to brief-extracted English instead:
- Added `brief_changelog(text)` — keeps only top-level `• ` bullets (drops sub-bullets), strips "by @user with full root cause + fix" / "from @user" prefatory phrases, replaces `[#nnn](url)` with `#nnn` for inline issue refs, cuts each bullet at the first natural sentence boundary (`:` after pos 30, `. `, ` — `), hard-caps at 200 chars per bullet, and trims any dangling unbalanced `(` or `[` left by the truncation.
- Both posts (files-channel announcement + main-channel cross-link) now use `english_brief = brief_changelog(english_notes)` instead of the full Persian.
- Title and footer chrome of both posts switched to English ("released" / "Files (Android, Windows, ...)" / "Channel:" / "or:").
The full Persian + full English text stays in `docs/changelog/v*.md` for archival; only the channel post becomes brief.
Verified locally on v1.9.7 / v1.9.8 / v1.9.9 — produces 246–458 char briefs with clean bullet structure, no dangling parens, no contributor noise.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Up through v1.9.7 the Telegram posts said only "📦 mhrv-rs vX.Y.Z منتشر شد" + a hashtag and a link to the files channel — subscribers had to click through to GitHub to see what actually changed. Now the announcement post in the files channel and the cross-link post in the main channel both inline the Persian half of `docs/changelog/v{version}.md`.
How it works:
- New `load_changelog(repo_root, version)` reads `docs/changelog/v{version}.md`, strips the leading `<!-- ... -->` editor comment, splits on the lone `---` line that separates Persian from English. Returns (None, None) if the file doesn't exist (lets out-of-band re-publishes for old tags whose changelog file was never landed work without crashing).
- New `md_to_tg_html(md, max_len)` does a minimal markdown → Telegram-flavoured-HTML conversion: `**bold**`, `[text](url)`, `` `code` `` are translated; nested patterns (e.g. `[`code`](url)`, `**[`code`](url)**`) work via a placeholder/unwind pass that loops until stable. Truncates at the 4096-char sendMessage limit, snapping to a newline boundary so a span isn't cut in half, with a "see full notes on GitHub" tail.
- Falls back gracefully if the changelog file is missing — uses the old skeleton message.
Verified locally on docs/changelog/v1.9.7.md: 3039 chars after conversion, well under the 4096 limit, all bold / code / link spans render correctly including nested ones (`[`assets/apps_script/Code.gs`](url)` becomes `<a href="url"><code>assets/apps_script/Code.gs</code></a>`).
This change takes effect on the next release (v1.9.8+); v1.9.7 is already published with the old skeleton.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes addressing user-reported issues today:
1. Exit-node feature ported from upstream masterking32@464a6e1d, with
hardening. Cloudflare-protected sites (chatgpt.com, claude.ai,
grok.com, x.com, openai.com) flag Google datacenter IPs as bots and
return Turnstile / CAPTCHA / 502 challenges. Apps Script's UrlFetchApp
exits from those IPs, so v1.9.3 surfaces these as "Relay error: json:
key must be a string..." with no apps_script-mode workaround.
Now a small TypeScript HTTP endpoint (assets/exit_node/valtown.ts)
deployed on val.town / Deno Deploy sits between Apps Script and the
destination. Chain: client → Apps Script (Google IP) → val.town
(non-Google IP) → destination. Destination sees val.town's IP, no
CF challenge.
Config:
"exit_node": {
"enabled": true,
"relay_url": "https://...web.val.run",
"psk": "<openssl rand -hex 32>",
"mode": "selective",
"hosts": ["chatgpt.com", "claude.ai", "x.com", "grok.com", "openai.com"]
}
Hardening over upstream: PSK fail-closed if still placeholder (fresh
deploy can't be open relay), loop guard (refuses fetch of own host),
explicit 503 on misconfigured. Fallback to direct Apps Script on exit
node failure (CF-affected sites fail, others keep working). Setup
docs in English + Persian at assets/exit_node/README*.md. Example
config at config.exit-node.example.json.
2. Removed the legacy `telegram` job from release.yml. With
TELEGRAM_NOTIFY_ENABLED repo var set to true, every release was
producing two duplicate APK posts on the main Telegram channel: the
old bundled-APK-on-main job AND the newer per-file files-channel
posts (telegram-publish-files.yml). Only the per-file flow is wanted.
Legacy job and its helper telegram_release_notify.py are gone.
Recoverable from git log if anyone needs the bundled pattern back.
169 mhrv-rs lib tests + 33 tunnel-node tests + UI build clean.
Two related changes to the main-channel cross-post (one message per
release that points at the files channel):
1. Post-link now uses the public-username form `t.me/mhrv_rs/<msg_id>`
instead of the private `t.me/c/<chat_id>/<msg_id>`. The latter only
resolves for channel members; the former works for everyone, so
recipients seeing the main-channel announcement can click through
to a specific release post even if they're not yet subscribed.
Wired via the existing `FILES_CHANNEL_USERNAME` workflow env var,
now defaulting to `mhrv_rs` (the channel's public username) if the
`vars.FILES_CHANNEL_USERNAME` repo variable is unset. Override per
repo if the channel is renamed.
2. Channel-join links rendered in the body of the main-channel post,
below the post-link:
لینک کانال:
https://t.me/mhrv_rs
و یا: https://t.me/+R1OyoHX2boA1ZDgx
Two forms cover the cases where one or the other is filtered:
- `t.me/mhrv_rs` — public username form, prettier, surfaces in
Telegram search
- `t.me/+<hash>` — invite link, the only join path that works for
private/restricted channels and for users whose client doesn't
resolve public usernames cleanly
Wired via new `FILES_CHANNEL_INVITE` env var, defaulting to the
current invite hash; override via `vars.FILES_CHANNEL_INVITE` if
rotated.
Per user request — not running this against v1.8.0 retroactively,
just wiring it up for the next release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two changes on top of last commit:
1. SHA-256 ("تایید اصالت") now in every file caption. Each artifact's
caption gets a `<code>...</code>` line with the file's SHA-256 hex
so recipients can `sha256sum <file>` after download and verify it
matches what the channel posted. Defends against modified copies
if the channel ever gets relayed through a third party.
For chunked uploads (file > 45 MB), each part shows BOTH:
- SHA-256 of that specific part (verifies the chunk downloaded
intact before bothering to reassemble)
- SHA-256 of the full reassembled file (verifies the final result
after `cat <name>.part_* > <name>`)
2. Main channel post is now a cross-link, not files.
Previously the legacy `telegram` job in release.yml posted the
universal APK + full changelog as one sendDocument + sendMessage
pair to the main announcement channel.
New behaviour: telegram-publish-files.yml's last step posts a short
message to the main channel saying "v1.8.0 released, click here
for files" with a t.me link pointing at the files channel's
announcement anchor post. Recipients land on the anchor, scroll
to find the platform-specific artifact they need.
Link format: `t.me/c/<chat_id>/<msg>` for private channels (works
for members), or `t.me/<username>/<msg>` if `FILES_CHANNEL_USERNAME`
repo variable is set (works for everyone — useful if the files
channel is later made public).
Legacy telegram job in release.yml stays in source, dormant,
gated on `vars.TELEGRAM_NOTIFY_ENABLED == 'true'` (default false).
Comment updated to note the new workflow is the canonical path.
If both are turned on at once, the main channel gets two posts
per release.
Tested manually for syntax + caption rendering — actual SHA-256 values
will appear on the next workflow_dispatch run.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New workflow + script that posts every artifact (Android APKs, Windows
ZIP, macOS .app + CLI tarballs, Linux glibc + musl, OpenWRT, Raspbian)
to the Telegram channel as separate sendDocument calls, each with a
Persian caption naming the platform variant and a `#v<NNN>` hashtag
(e.g. `#v180`, `#v1810`, `#v200`) so users can find a specific release
later via the channel's hashtag search.
Files larger than 45 MB (the Bot API's effective ceiling after multipart
+ caption overhead) are split into byte chunks named `<name>.part_aa`,
`.part_ab`, ... and posted with reassembly instructions in the caption.
For the v1.8.0 file set everything is ≤41 MB so the split path is
defensive.
Decoupled from `release.yml` so it can be re-triggered for any past tag
via `workflow_dispatch` without rebuilding artifacts — downloads from
the GitHub Release page directly via `gh release download`. Also
auto-runs on each successful `release.yml` completion via
`workflow_run`.
Hard-codes the channel ID `-1003966234444` (one well-known channel,
auditable in source). Reuses `secrets.TELEGRAM_BOT_TOKEN` which already
has post permissions there.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Telegram release notifier used to post just the universal APK with
a single-document caption. This change ships the per-platform binaries
for macOS (amd64+arm64 CLI), Linux (amd64+arm64 CLI), Windows
(amd64 UI), and Android (universal APK) as a single Telegram media
group with one caption listing every filename + SHA-256.
Workflow side (.github/workflows/release.yml):
- The telegram job now downloads ALL artifacts (was: APK only).
- New `Prepare files for Telegram media group` step extracts the raw
binaries out of each per-platform .tar.gz / .zip (no archive
wrappers in the channel) and renames them with version suffixes
(mhrv-rs-linux-amd64-v1.7.2, mhrv-rs-windows-amd64-ui-v1.7.2.exe,
etc.). Per-platform extraction is best-effort: a missing artifact
emits a `::warning::` and skips that platform rather than failing
the whole post.
- The post step builds a `--files <path>` arg list from tg-files/,
sorted for deterministic order across runs, and invokes the
notifier without --with-changelog (the script auto-replies with
changelog whenever --files is used).
Script side (.github/scripts/telegram_release_notify.py):
- New --files arg (repeatable). 2..=10 files → sendMediaGroup; 1 file
→ sendDocument with the same caption shape; 0 → error. Telegram's
sendMediaGroup rejects single-item groups, so the 1-file fallback
isn't optional.
- New build_media_group_caption() composes title + per-file
filename+SHA list + repo/release URLs. Fits ~860 chars for a 6-file
release; fallback to filename-only-list if a future swell pushes
past Telegram's 1024-char caption cap.
- send_media_group() handles the multipart/form-data shape with each
file referenced as `attach://fileN` from the media JSON. Caption is
attached to file 0 only (Telegram clients render per-item captions
inconsistently for media groups; first-item-only is the safe
pattern).
- Legacy --apk path kept for any caller that hasn't migrated; either
--apk or --files must be present (validated at startup).
- _content_type_for() picks application/vnd.android.package-archive
for .apk and application/octet-stream for everything else, so
Telegram clients label the APK with the Android icon and label
desktop binaries by filename without a misleading icon.
Behavioural change for users:
- The Telegram channel now sees one grouped post per release with all
primary platform binaries inline, instead of just the APK. macOS
users wanting the gatekeeper-friendly .app.zip still grab it from
the GitHub Releases page; the Telegram drop is for the "give me
the binary, I'll run it" path.
- The Persian/English changelog reply that used to be opt-in (via
TELEGRAM_INCLUDE_CHANGELOG=true) is now automatic in the --files
path because the per-file SHA list eats the caption budget that
previously held the FA brief-note.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Ships PR #173 (event-driven drain) plus three operational improvements:
PR #173 — long-poll tunnel mode. The tunnel-node's batch drain
switched from a fixed 150 ms sleep to an event-driven Notify wait;
idle sessions long-poll up to 5 s and wake on the first byte from
upstream. Push notifications and chat messages now arrive in roughly
RTT instead of waiting for the next client poll tick. Backward compat
with pre-#173 tunnel-nodes is automatic via a sticky AtomicBool that
detects fast empty replies and reverts to the legacy cadence.
92 client tests + 17 tunnel-node tests pass, including end-to-end
TCP-pair verification of the notify wiring.
Docker image for tunnel-node. Adds a hardened Dockerfile (BuildKit
cache mounts, non-root runtime user, ca-certificates for HTTPS
upstreams) and a .dockerignore to keep build context small. New
`tunnel-docker` job in the release workflow builds + pushes
multi-arch (linux/amd64 + linux/arm64) to
ghcr.io/therealaleph/mhrv-tunnel-node with `:latest`, `:1.5`, and
`:1.5.0` tags on every release. Setting up Full Tunnel mode goes
from "rustup + cargo build on a 1 GB VPS" (which fails on memory
half the time) to a one-liner. tunnel-node/README.md updated with
prebuilt-image + docker-compose recipes.
Brief Persian release note in Telegram caption. The release-post
caption now leads with a `<blockquote>`-wrapped FA bullet headlines
extracted from `docs/changelog/v<ver>.md`, above the existing two
links (repo + release). Markdown links → Telegram HTML <a> for
clickability. Cap-budget-aware truncation at bullet boundaries
keeps total caption under Telegram's 1024-char limit. Headlines-only
rather than full bullets so multiple "what's new" items fit
comfortably (the full bullets remain on the GH release page and as
the optional --with-changelog reply-threaded message).
GitHub Releases page bodies now lead with the changelog content
(Persian section + `---` + English) instead of just a Full Changelog
comparison link. The auto comparison link is appended at the bottom
via `append_body: true` rather than removed.
Workflow changes:
- New `permissions: packages: write` at the workflow level (required
for ghcr push via docker/login-action).
- New `tunnel-docker` job needs `build` (not the full matrix) to
serialize the QEMU buildx layer with the matrix cache.
- Release job composes the body from `docs/changelog/v${VER}.md`
in a pre-step that handles both tag-push and workflow_dispatch
paths (uses inputs.version || github.ref_name like the rest of
the workflow).
Tested locally:
- `cargo test` — 92 lib tests pass
- `cargo test -p mhrv-tunnel-node` — 17 tests pass
- `docker build` of tunnel-node Dockerfile — 32 MB image, runs as
non-root, /health returns "ok", auth rejection works correctly,
legitimate requests open sessions to remote hosts
- Telegram script `--dry-run` mode added; rendered captions for
v1.4.0, v1.4.1, v1.5.0 all fit under 900 chars
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Rolls up the four post-v1.2.14 commits on main into a single tagged
release. Highlights:
- Per-deployment concurrency (#142): each deployment ID gets its own
30-permit semaphore, so setups with deployments across multiple
Google accounts get a genuine 30×N throughput ceiling. Single-account
setups still cap at Google's per-account 30-simultaneous limit —
docs (EN + FA) updated to call that out.
- Android app-splitting ONLY-mode bug fix (#143): the previous code
called both addAllowedApplication and addDisallowedApplication,
which Android documents as mutually exclusive. ONLY mode was
silently failing establish(). Now fixed.
- Per-ABI Android APKs (#136): ships four split APKs (arm64-v8a ~21 MB,
armeabi-v7a ~18 MB, x86_64 ~23 MB, x86 ~22 MB) alongside the ~53 MB
universal. Huge distribution win for users on unreliable
censorship-tunnel paths — the 21 MB arm64-v8a download succeeds
where the universal doesn't.
- Honest IP-exposure note in Security Posture (#148): clarified that
v1.2.9's forwarded-header stripping only covers the client-side leg;
what Google's own infrastructure may add on the UrlFetchApp.fetch()
second leg is outside this client's control. Full Tunnel mode is
the recommendation for threat models where that matters.
- Telegram release-post format: added Persian preambles above both
links (GitHub repo + full Persian guide; release page + desktop/
router builds) so channel readers see the intent at a glance.
82 tests pass. Desktop + Android builds both verified clean locally
across the v1.2.15+ commit series.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contains the three safety fixes from PRs #48/#49/#50 and the Persian
README RTL polishing from #58, all squashed into main. Merge details
already in their individual PR comments; summary:
#48: reject truncated Content-Length relay responses (previously
silently accepted whatever bytes arrived before EOF)
#49: reject truncated or malformed (missing CRLF) chunked-encoding
relay responses (same class of silent-acceptance bug)
#50: restrict the SNI-rewrite tunnel dispatch to port 443. Plain
HTTP (:80) targets that happened to match google.com / hosts
override were being steered into the TLS tunnel and blocking
waiting for a ClientHello that would never arrive.
#58: trailing-whitespace line-breaks on Persian bullet lists in
README so the RTL rendering doesn't collapse consecutive
items into a single paragraph.
Test suite grew from 54 to 58 passing (three new negative tests for
the relay-reader correctness fixes + one SNI-rewrite port filter).
Telegram CI notify default switched to file-plus-link:
- script gains a `--with-changelog` flag; default OFF
- workflow only passes it when `vars.TELEGRAM_INCLUDE_CHANGELOG=true`
- every routine release now posts just the APK + short caption
(title + SHA-256 + repo URL + release URL) with no long body
To include bullets for a given release again:
gh variable set TELEGRAM_INCLUDE_CHANGELOG --body true
The existing `vars.TELEGRAM_NOTIFY_ENABLED` job-level gate remains —
changelog toggle is orthogonal to enable/disable.
Also closes PR #55 without merging; ads/analytics domains were being
lumped under a YouTube-specific toggle, and the PR committed per-
machine \`.cargo/config.toml\` + zig-cc cross-compile helpers that
would have broken CI on actual Windows / macOS runners.
The v1.1.0 CI telegram job failed with curl exit 26 "Failed to
open/read local data from file/application" because:
-F "caption=<b>mhrv-rs Android v1.1.0</b>..."
curl's -F treats a value starting with `<` as "read from file
named ..." (the canonical way to put file CONTENTS into a text
form field). Our HTML captions start with `<b>`, so curl tried
to open a file named `b>mhrv-rs Android v1.1.0</b>...`, failed,
and the whole job went red.
Rewrote the step in Python (`.github/scripts/telegram_release_notify.py`).
stdlib urllib + http.client have no such value-interpretation
wart. Also:
- uses `application/vnd.android.package-archive` content-type
so Telegram shows the APK with an Android-package label, not
generic octet-stream
- proper sha256 hash (streaming, not shell-piped)
- consolidated the two shell-script HEREDOCs that were parsing
the changelog into one place
- clean exit codes: "no changelog file" and "no secrets" both
exit 0, a broken Telegram response exits non-zero
No behaviour change for callers — the workflow just calls the
script with the same four inputs.