该提交新增了完整的BlueBubbles渠道插件,支持通过BlueBubbles Server集成iMessage功能,包含以下核心能力: 1. 支持私聊和群聊会话管理,自动区分会话类型 2. 完整的消息收发支持,包括文本、图片、语音、文件、视频消息 3. 支持消息反应、已读回执、消息编辑与撤回 4. 内置去重、防抖处理机制 5. 支持Webhook和WebSocket两种事件接收方式 6. 完善的权限与安全校验机制 7. 历史消息同步与抓包功能 8. TTS语音合成与发送支持 9. 群组管理能力,包括重命名、修改头像、增减成员等
74 lines
2.1 KiB
Python
74 lines
2.1 KiB
Python
import logging
|
|
import os
|
|
from pathlib import Path
|
|
|
|
from yuxi.channel.extensions.bluebubbles.client import BlueBubblesClient
|
|
from yuxi.channel.extensions.bluebubbles.outbound import send_attachment
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SUPPORTED_AUDIO_FORMATS = frozenset({"caf", "mp3", "m4a", "wav", "aac"})
|
|
PREFERRED_FORMAT = "caf"
|
|
MAX_VOICE_MEMO_MB = 5.0
|
|
VOICE_MEMO_MIME = "audio/x-caf"
|
|
|
|
|
|
def _audio_file_ext(file_path: str) -> str:
|
|
return Path(file_path).suffix.lower().lstrip(".")
|
|
|
|
|
|
def _audio_file_size_mb(file_path: str) -> float:
|
|
try:
|
|
return os.path.getsize(file_path) / (1024 * 1024)
|
|
except OSError:
|
|
return 0.0
|
|
|
|
|
|
def validate_voice_input(file_path: str) -> None:
|
|
if not os.path.isfile(file_path):
|
|
raise FileNotFoundError(f"Voice memo file not found: {file_path}")
|
|
|
|
ext = _audio_file_ext(file_path)
|
|
if ext not in SUPPORTED_AUDIO_FORMATS:
|
|
raise ValueError(f"Unsupported audio format '.{ext}'. Supported: {', '.join(sorted(SUPPORTED_AUDIO_FORMATS))}")
|
|
|
|
size_mb = _audio_file_size_mb(file_path)
|
|
if size_mb > MAX_VOICE_MEMO_MB:
|
|
raise ValueError(f"Voice memo too large: {size_mb:.1f}MB (max {MAX_VOICE_MEMO_MB}MB)")
|
|
|
|
if ext != PREFERRED_FORMAT:
|
|
logger.warning(
|
|
"Voice memo format is '.%s', prefer '.%s' to avoid BlueBubbles Server CAF->MP3 race condition",
|
|
ext,
|
|
PREFERRED_FORMAT,
|
|
)
|
|
|
|
|
|
async def send_voice_memo(
|
|
client: BlueBubblesClient,
|
|
chat_guid: str,
|
|
audio_file_path: str,
|
|
*,
|
|
private_api_available: bool = False,
|
|
) -> dict:
|
|
validate_voice_input(audio_file_path)
|
|
|
|
ext = _audio_file_ext(audio_file_path)
|
|
mime_type = "audio/x-caf" if ext == "caf" else f"audio/{ext}"
|
|
|
|
logger.info(
|
|
"Sending iMessage voice memo: chat=%s format=%s size=%.1fMB",
|
|
chat_guid,
|
|
ext,
|
|
_audio_file_size_mb(audio_file_path),
|
|
)
|
|
|
|
return await send_attachment(
|
|
client,
|
|
chat_guid,
|
|
audio_file_path,
|
|
mime_type=mime_type,
|
|
as_voice=True,
|
|
private_api_available=private_api_available,
|
|
)
|