新增 Zalo OA、Zoom Chat、Zulip 三个渠道扩展。 Zalo OA 渠道扩展主要模块:sidecar_client, config, gateway, outbound, streaming, pairing, security, auth, dedupe, directory, monitor, status, session, reactions, tools Zoom Chat 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, crypto, dedupe, actions, media, mentions, monitor, status, session, reactions, threading Zulip 渠道扩展主要模块:client, config, gateway, outbound, streaming, pairing, security, monitor, status
134 lines
4.2 KiB
Python
134 lines
4.2 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Any
|
|
|
|
ZALO_TEXT_LIMIT = 2000
|
|
|
|
_FORMAT_TAG_PATTERN = re.compile(r"\{(\w+)\}(.*?)\{/(\w+)\}", re.DOTALL)
|
|
|
|
|
|
def parse_zalouser_text_styles(markdown: str) -> list[dict[str, Any]]:
|
|
styles: list[dict[str, Any]] = []
|
|
|
|
code_block_pattern = re.compile(r"```[\s\S]*?```")
|
|
code_blocks = code_block_pattern.findall(markdown)
|
|
placeholder = "\u0000"
|
|
text = markdown
|
|
for cb in code_blocks:
|
|
text = text.replace(cb, placeholder, 1)
|
|
|
|
def _capture_style(start: int, end: int, style_type: str) -> None:
|
|
styles.append(
|
|
{
|
|
"start": start,
|
|
"end": end,
|
|
"type": style_type,
|
|
}
|
|
)
|
|
|
|
def _apply_regex(pat: re.Pattern, style_type: str, text_cursor: str) -> str:
|
|
offset = 0
|
|
result = list(text_cursor)
|
|
for m in pat.finditer(text_cursor):
|
|
inner = m.group(2) if pat.groups >= 2 else m.group(1)
|
|
raw_start = m.start() - offset
|
|
raw_end = raw_start + len(inner)
|
|
_capture_style(raw_start, raw_end, style_type)
|
|
result[m.start() - offset : m.end() - offset] = [inner]
|
|
offset += m.end() - m.start() - len(inner)
|
|
return "".join(result)
|
|
|
|
text = _apply_regex(re.compile(r"\*\*(.+?)\*\*"), "b", text)
|
|
text = _apply_regex(re.compile(r"__(.+?)__"), "u", text)
|
|
text = _apply_regex(re.compile(r"\*(.+?)\*"), "i", text)
|
|
text = _apply_regex(re.compile(r"~~(.+?)~~"), "s", text)
|
|
|
|
heading_pat = re.compile(r"^#{1,3}\s+(.+)$", re.MULTILINE)
|
|
for m in heading_pat.finditer(text):
|
|
_capture_style(m.start(1), m.end(1), "b")
|
|
_capture_style(m.start(1), m.end(1), "f_18")
|
|
|
|
custom_tag_pat = re.compile(r"\{(\w+)\}(.*?)\{/(\w+)\}", re.DOTALL)
|
|
tag_style_map = {
|
|
"red": "c_db342e",
|
|
"orange": "c_f27806",
|
|
"yellow": "c_f7b503",
|
|
"green": "c_15a85f",
|
|
"blue": "c_4391d9",
|
|
"big": "f_18",
|
|
}
|
|
for m in custom_tag_pat.finditer(text):
|
|
tag = m.group(1)
|
|
if tag in tag_style_map:
|
|
_capture_style(m.start(2), m.end(2), tag_style_map[tag])
|
|
|
|
pid = 0
|
|
for pid, ph in enumerate(code_blocks):
|
|
text = text.replace(placeholder, ph, 1)
|
|
|
|
return styles
|
|
|
|
|
|
def chunk_text_for_outbound(text: str, limit: int = ZALO_TEXT_LIMIT) -> list[str]:
|
|
if len(text) <= limit:
|
|
return [text]
|
|
|
|
chunks: list[str] = []
|
|
paragraphs = text.split("\n\n")
|
|
|
|
current = ""
|
|
for para in paragraphs:
|
|
if len(current) + len(para) + 2 <= limit:
|
|
current = f"{current}\n\n{para}" if current else para
|
|
else:
|
|
if current:
|
|
chunks.append(current)
|
|
if len(para) <= limit:
|
|
current = para
|
|
else:
|
|
current = ""
|
|
para_remain = para
|
|
while len(para_remain) > limit:
|
|
split_at = para_remain.rfind("\n", 0, limit)
|
|
if split_at == -1:
|
|
split_at = para_remain.rfind(" ", 0, limit)
|
|
if split_at == -1:
|
|
split_at = limit
|
|
chunks.append(para_remain[:split_at].strip())
|
|
para_remain = para_remain[split_at:].strip()
|
|
if para_remain:
|
|
current = para_remain
|
|
|
|
if current:
|
|
chunks.append(current)
|
|
|
|
return chunks if chunks else [text[:limit]]
|
|
|
|
|
|
def resolve_reaction_icon(raw: str) -> str | None:
|
|
alias = raw.strip().lower()
|
|
from yuxi.channel.extensions.zalouser.types import REACTION_ALIAS_MAP
|
|
|
|
if alias in REACTION_ALIAS_MAP:
|
|
return REACTION_ALIAS_MAP[alias].value
|
|
for icon in REACTION_ALIAS_MAP.values():
|
|
if icon.value.lower() == alias:
|
|
return icon.value
|
|
return None
|
|
|
|
|
|
def strip_own_mentions(content: str, bot_name: str = "") -> str:
|
|
if not bot_name:
|
|
return content
|
|
pattern = re.compile(rf"@{re.escape(bot_name)}\s*", re.IGNORECASE)
|
|
return pattern.sub("", content).strip()
|
|
|
|
|
|
def extract_mention_ids(content: str) -> list[str]:
|
|
pattern = re.compile(r"@(\w+)")
|
|
return pattern.findall(content)
|
|
|
|
|
|
def format_for_zalouser_markdown(markdown: str) -> str:
|
|
return markdown |