ci(telegram): use public mhrv_rs link in main-channel post + add invite

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>
This commit is contained in:
therealaleph
2026-04-28 03:30:37 +03:00
parent 0669b9310c
commit 0d54c5c6fb
2 changed files with 73 additions and 17 deletions
+57 -11
View File
@@ -339,24 +339,51 @@ def files_channel_post_link(chat_id: str, message_id: int) -> str:
def post_main_channel_pointer( def post_main_channel_pointer(
bot_token: str, bot_token: str,
main_chat_id: str, main_chat_id: str,
files_channel_link: str, files_channel_post_link: str,
version: str, version: str,
hashtag: str, hashtag: str,
channel_username_link: str = "",
channel_invite_link: str = "",
) -> bool: ) -> bool:
"""Post a short cross-link to the main announcement channel pointing """Post a short cross-link to the main announcement channel pointing
at the anchor post in the files channel. Replaces the previous at the anchor post in the files channel. Replaces the previous
behaviour of posting the universal APK + full changelog directly behaviour of posting the universal APK + full changelog directly
to the main channel — the main channel becomes a discovery surface to the main channel — the main channel becomes a discovery surface
while the files channel hosts the actual artifacts. while the files channel hosts the actual artifacts.
Includes channel-join links (public username + invite hash) at the
bottom so recipients who aren't yet members can subscribe before
clicking through to the specific release post.
""" """
text = ( parts = [
f"<b>📦 mhrv-rs v{html_escape(version)} منتشر شد</b>\n" f"<b>📦 mhrv-rs v{html_escape(version)} منتشر شد</b>",
f"\nبرای دانلود فایل‌ها (Android، Windows، macOS، Linux و ...) " "",
f"به کانال فایل‌ها مراجعه کنید:\n" f"برای دانلود فایل‌ها (Android، Windows، macOS، Linux و ...) "
f"\n👉 <a href=\"{html_escape(files_channel_link)}\">" f"به کانال فایل‌ها مراجعه کنید:",
f"v{html_escape(version)} — همه فایل‌ها + SHA-256</a>\n" "",
f"\n{hashtag}" f"👉 <a href=\"{html_escape(files_channel_post_link)}\">"
) f"v{html_escape(version)} — همه فایل‌ها + SHA-256</a>",
]
# Channel-join links. Two forms handle different states of the
# files channel: the `t.me/<username>` form works for public
# channels and is the prettier link; the `t.me/+<hash>` invite
# link works regardless of whether the channel is public, and is
# the only path in for private/restricted channels. Showing both
# is forgiving — recipients click whichever works for them.
if channel_username_link or channel_invite_link:
parts.append("")
parts.append("لینک کانال:")
if channel_username_link:
# Render as plain URL (not HTML <a>) so the text shows the
# link itself — useful when users share the message via
# screenshot or copy-paste outside Telegram, which would
# strip the <a href> wrapper.
parts.append(html_escape(channel_username_link))
if channel_invite_link:
parts.append(f"و یا: {html_escape(channel_invite_link)}")
parts.append("")
parts.append(hashtag)
text = "\n".join(parts)
url = f"https://api.telegram.org/bot{bot_token}/sendMessage" url = f"https://api.telegram.org/bot{bot_token}/sendMessage"
data = urllib.parse.urlencode({ data = urllib.parse.urlencode({
"chat_id": main_chat_id, "chat_id": main_chat_id,
@@ -472,11 +499,30 @@ def main() -> int:
main_chat_id = os.environ.get("MAIN_CHAT_ID", "").strip() main_chat_id = os.environ.get("MAIN_CHAT_ID", "").strip()
if main_chat_id and announce_msg_id is not None: if main_chat_id and announce_msg_id is not None:
link = files_channel_post_link(chat_id, announce_msg_id) link = files_channel_post_link(chat_id, announce_msg_id)
# Optional channel-join links rendered alongside the cross-link.
# `FILES_CHANNEL_USERNAME` is the public-username form (clean
# `t.me/<name>` URL — clickable for everyone). `FILES_CHANNEL_INVITE`
# is the `t.me/+<hash>` invite link, the only join path for
# private channels. Either or both can be set; both render in
# the body as separate lines.
username = os.environ.get("FILES_CHANNEL_USERNAME", "").strip().lstrip("@")
username_link = f"https://t.me/{username}" if username else ""
invite_link = os.environ.get("FILES_CHANNEL_INVITE", "").strip()
print() print()
print(f"posting cross-link to main channel:") print(f"posting cross-link to main channel:")
print(f" link: {link}") print(f" post link: {link}")
if username_link:
print(f" channel username link: {username_link}")
if invite_link:
print(f" channel invite link: {invite_link}")
ok = post_main_channel_pointer( ok = post_main_channel_pointer(
bot_token, main_chat_id, link, args.version, args.hashtag bot_token,
main_chat_id,
link,
args.version,
args.hashtag,
channel_username_link=username_link,
channel_invite_link=invite_link,
) )
if not ok: if not ok:
failures += 1 failures += 1
+16 -6
View File
@@ -110,12 +110,22 @@ jobs:
# APK + full changelog. Sourced from the same secret the # APK + full changelog. Sourced from the same secret the
# legacy `telegram` job in release.yml used. # legacy `telegram` job in release.yml used.
MAIN_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} MAIN_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
# Optional: if the files channel later gets a public username, # Public-username form of the files channel link. Used for
# set the repo variable `FILES_CHANNEL_USERNAME` (without the # both (a) the post-link in the main-channel cross-post — so
# `@`) so the cross-link uses the prettier `t.me/<name>/<msg>` # `t.me/<name>/<msg>` works for everyone, not just members
# form instead of `t.me/c/<id>/<msg>` (which only resolves for # via `t.me/c/<id>/<msg>` — and (b) one of the two
# channel members). # channel-join links rendered at the bottom of the cross-post.
FILES_CHANNEL_USERNAME: ${{ vars.FILES_CHANNEL_USERNAME }} # Defaults to `mhrv_rs` (current public username); override via
# repo variable if the channel is renamed.
FILES_CHANNEL_USERNAME: ${{ vars.FILES_CHANNEL_USERNAME || 'mhrv_rs' }}
# `t.me/+<hash>` invite link for the files channel. Rendered
# as the second channel-join option in the main-channel
# cross-post — the only join path that works for users coming
# from outside Telegram search (private/restricted channels)
# or whose Telegram client doesn't resolve usernames cleanly.
# Override via repo variable if the channel's invite hash is
# rotated.
FILES_CHANNEL_INVITE: ${{ vars.FILES_CHANNEL_INVITE || 'https://t.me/+R1OyoHX2boA1ZDgx' }}
run: | run: |
if [ -z "${BOT_TOKEN:-}" ]; then if [ -z "${BOT_TOKEN:-}" ]; then
echo "::error::TELEGRAM_BOT_TOKEN not set; can't publish" echo "::error::TELEGRAM_BOT_TOKEN not set; can't publish"