ForcePilot/backend/package/yuxi/channels/adapters/bluebubbles/renderer.py
Kris 6aa1d52a8b feat(bluebubbles): 实现完整的BlueBubbles适配器基础组件
新增BlueBubbles适配器全套核心工具类与服务,包含会话管理、消息去重、Webhook验证、缓存系统、账号配置解析、聊天消息处理等完整功能模块,支持iMessage消息收发、群管理、反应特效、语音合成等能力,提供完善的健康检查与配置校验流程。
2026-05-12 00:42:57 +08:00

101 lines
2.9 KiB
Python

from __future__ import annotations
import re
_ALLOWED_TAGS = {"b", "i", "u", "s", "a", "br", "code", "pre", "blockquote", "img"}
_UNSUPPORTED_REPLACEMENTS: dict[str, tuple[str, str]] = {
"strong": ("<b>", "</b>"),
"em": ("<i>", "</i>"),
"strike": ("<s>", "</s>"),
"del": ("<s>", "</s>"),
"ins": ("<u>", "</u>"),
"h1": ("<b>", "</b>\n"),
"h2": ("<b>", "</b>\n"),
"h3": ("<b>", "</b>\n"),
"h4": ("<b>", "</b>\n"),
"h5": ("<b>", "</b>\n"),
"h6": ("<b>", "</b>\n"),
"p": ("\n", ""),
"ul": ("", ""),
"ol": ("", ""),
"li": ("", "\n"),
"span": ("", ""),
"div": ("\n", ""),
"hr": ("\n---\n", ""),
}
_STRIP_TAGS = {"script", "style", "head", "meta", "link", "iframe", "object", "embed"}
def render_html_to_imessage(html: str) -> str:
if not html:
return ""
for tag in _STRIP_TAGS:
html = re.sub(rf"<{tag}[^>]*>.*?</{tag}>", "", html, flags=re.DOTALL | re.IGNORECASE)
html = re.sub(rf"<{tag}[^>]*/>", "", html, flags=re.IGNORECASE)
for tag, (open_tag, close_tag) in _UNSUPPORTED_REPLACEMENTS.items():
html = re.sub(rf"<{tag}(\s[^>]*)?>", open_tag, html, flags=re.IGNORECASE)
html = re.sub(rf"</{tag}>", close_tag, html, flags=re.IGNORECASE)
html = _strip_unsafe_attributes(html)
html = _normalize_links(html)
html = html.replace("\r\n", "\n").replace("\r", "\n")
return html.strip()
def _strip_unsafe_attributes(html: str) -> str:
def _clean_tag(tag_match: re.Match) -> str:
full_tag = tag_match.group(0)
tag_name_match = re.match(r"</?(\w+)", full_tag)
if not tag_name_match:
return full_tag
tag_name = tag_name_match.group(1).lower()
if tag_name not in _ALLOWED_TAGS:
return full_tag
safe_attrs = ["href", "src", "alt"]
result = full_tag
for attr in re.finditer(r'(\w+)="([^"]*)"', full_tag):
if attr.group(1).lower() not in safe_attrs:
result = result.replace(attr.group(0), "")
return result
return re.sub(r"<[^>]+>", _clean_tag, html)
def _normalize_links(text: str) -> str:
url_pattern = re.compile(r"(?<!href=\")(https?://[^\s<>\"\)]+)")
return url_pattern.sub(r'<a href="\1">\1</a>', text)
def render_markdown_to_imessage(text: str) -> str:
if not text:
return ""
text = text.replace("**", "<b>", 1)
while "**" in text:
text = text.replace("**", "</b>", 1)
if "**" in text:
text = text.replace("**", "<b>", 1)
text = text.replace("__", "<b>", 1)
while "__" in text:
text = text.replace("__", "</b>", 1)
if "__" in text:
text = text.replace("__", "<b>", 1)
text = text.replace("*", "<i>", 1)
while "*" in text:
text = text.replace("*", "</i>", 1)
text = text.replace("_", "<i>", 1)
while "_" in text:
text = text.replace("_", "</i>", 1)
return text