Publish Release to Telegram #6
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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("<") | |
| elif text[i] == ">": | |
| result.append(">") | |
| elif text[i] == "&": | |
| result.append("&") | |
| 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 |