Skip to content

Publish Release to Telegram #6

Publish Release to Telegram

Publish Release to Telegram #6

name: Publish Release to Telegram
on:
release:
types: [published]
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send release note and assets to Telegram
env:
BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
CHANNEL_ID: ${{ secrets.TELEGRAM_CHANNEL_ID }}
run: |
python3 << 'PYEOF'
import html as html_module
import json
import os
import re
import urllib.error
import urllib.request
BOT_TOKEN = os.environ["BOT_TOKEN"]
CHANNEL_ID = os.environ["CHANNEL_ID"]
EVENT_PATH = os.environ["GITHUB_EVENT_PATH"]
# Telegram HTML parse_mode only allows these tags.
ALLOWED_TAGS = {"b", "i", "u", "s", "code", "pre", "a", "br", "tg-spoiler"}
def escape_html_except_allowed(text):
"""Escape text content while preserving Telegram-supported HTML tags."""
result = []
i = 0
while i < len(text):
if text[i] == "<":
match = re.match(r"</?([a-zA-Z][a-zA-Z0-9]*)(?:\s+[^>]*)?>", text[i:])
if match and match.group(1).lower() in ALLOWED_TAGS:
result.append(text[i:i + match.end()])
i += match.end()
continue
result.append("&lt;")
elif text[i] == ">":
result.append("&gt;")
elif text[i] == "&":
result.append("&amp;")
else:
result.append(text[i])
i += 1
return "".join(result)
def markdown_to_telegram_html(text):
"""Convert the release body's GitHub Markdown subset to Telegram HTML."""
if not text:
return ""
placeholders = {}
ph_counter = [0]
def protect(content, prefix):
key = f"\x00{prefix}{ph_counter[0]}\x00"
ph_counter[0] += 1
placeholders[key] = content
return key
text = re.sub(
r"```(?:\w+)?\n?(.*?)```",
lambda m: protect(m.group(1), "CB"),
text,
flags=re.DOTALL,
)
text = re.sub(r"`([^`]+?)`", lambda m: protect(m.group(1), "IC"), text)
text = re.sub(r"^#{1,6}\s+(.+)$", r"<b>\1</b>", text, flags=re.MULTILINE)
text = re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", text)
text = re.sub(r"__(.+?)__", r"<b>\1</b>", text)
text = re.sub(r"(?<!\*)\*(?!\*)(.+?)(?<!\*)\*(?!\*)", r"<i>\1</i>", text)
text = re.sub(r"(?<!_)_(?!_)(.+?)(?<!_)_(?!_)", r"<i>\1</i>", text)
text = re.sub(r"~~(.+?)~~", r"<s>\1</s>", text)
text = re.sub(r"\[([^\]]+?)\]\(([^)]+?)\)", r'<a href="\2">\1</a>', text)
text = re.sub(r"^(\s*)[-*]\s+(.+)$", r"\1• \2", text, flags=re.MULTILINE)
for key, content in placeholders.items():
escaped = html_module.escape(content)
if key.startswith("\x00CB"):
text = text.replace(key, f"<pre>{escaped}</pre>")
else:
text = text.replace(key, f"<code>{escaped}</code>")
text = escape_html_except_allowed(text)
return text.replace("\n", "<br>")
with open(EVENT_PATH) as f:
release = json.load(f)["release"]
body_html = markdown_to_telegram_html(release.get("body", "") or "")
text = f"<b>{html_module.escape(release['name'])}</b><br><br>{body_html}"
if len(text) > 4000:
truncate_at = text.rfind("<br>", 0, 3950)
if truncate_at == -1:
truncate_at = 3950
text = text[:truncate_at] + "<br><br><i>... (truncated)</i>"
text += f"<br><br><a href='{release['html_url']}'>View on GitHub</a>"
req = urllib.request.Request(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
data=json.dumps({
"chat_id": CHANNEL_ID,
"text": text,
"parse_mode": "HTML",
"disable_web_page_preview": False,
}).encode(),
headers={"Content-Type": "application/json"},
)
resp = urllib.request.urlopen(req)
msg_id = json.load(resp)["result"]["message_id"]
try:
urllib.request.urlopen(urllib.request.Request(
f"https://api.telegram.org/bot{BOT_TOKEN}/pinChatMessage",
data=json.dumps({
"chat_id": CHANNEL_ID,
"message_id": msg_id,
"disable_notification": True,
}).encode(),
headers={"Content-Type": "application/json"},
))
except urllib.error.HTTPError as e:
print(f"Failed to pin message: {e.read().decode()}")
for asset in release.get("assets", []):
size_kb = asset.get("size", 0) // 1024
payload = json.dumps({
"chat_id": CHANNEL_ID,
"document": asset["browser_download_url"],
"caption": f"<code>{html_module.escape(asset['name'])}</code> ({size_kb}KB)",
"parse_mode": "HTML",
"reply_to_message_id": msg_id,
}).encode()
try:
urllib.request.urlopen(urllib.request.Request(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendDocument",
data=payload,
headers={"Content-Type": "application/json"},
))
except urllib.error.HTTPError as e:
print(f"Failed to send {asset['name']}: {e.read().decode()}")
print("Done: Telegram notification sent")
PYEOF