该提交新增了基于BlueBubbles的iMessage渠道插件,支持单聊和群组消息,包含文本、图片、语音、文件和视频消息收发,支持消息编辑、撤回、回复、 reactions和输入状态提示,同时实现了账号配置、安全校验、配对授权、消息格式化与分片等完整功能。
105 lines
2.8 KiB
Python
105 lines
2.8 KiB
Python
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
|
|
from yuxi.channel.extensions.imessage.errors import IMessageMediaError
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_MEDIA_MAX_MB = 16
|
|
|
|
|
|
async def download_media(
|
|
media_url: str,
|
|
max_mb: int = DEFAULT_MEDIA_MAX_MB,
|
|
temp_dir: str | None = None,
|
|
) -> tuple[bytes, str, str]:
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(60.0)) as client:
|
|
resp = await client.get(media_url)
|
|
resp.raise_for_status()
|
|
data = resp.content
|
|
|
|
size_mb = len(data) / (1024 * 1024)
|
|
if size_mb > max_mb:
|
|
raise IMessageMediaError(
|
|
f"Media size {size_mb:.1f}MB exceeds limit of {max_mb}MB"
|
|
)
|
|
|
|
content_type = resp.headers.get("content-type", "application/octet-stream")
|
|
ext = _guess_extension(content_type)
|
|
filename = f"imessage_media_{os.urandom(8).hex()}{ext}"
|
|
|
|
write_dir = temp_dir or tempfile.gettempdir()
|
|
filepath = os.path.join(write_dir, filename)
|
|
Path(filepath).write_bytes(data)
|
|
|
|
return data, content_type, filepath
|
|
|
|
|
|
def cleanup_temp_file(filepath: str) -> None:
|
|
try:
|
|
os.unlink(filepath)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
async def download_attachment_via_api(
|
|
attachment_guid: str,
|
|
server_url: str,
|
|
password: str,
|
|
max_mb: int = DEFAULT_MEDIA_MAX_MB,
|
|
) -> tuple[bytes, str]:
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(60.0)) as client:
|
|
resp = await client.get(
|
|
f"{server_url}/api/v1/attachment/{attachment_guid}/download",
|
|
headers={"Password": password},
|
|
)
|
|
resp.raise_for_status()
|
|
data = resp.content
|
|
|
|
size_mb = len(data) / (1024 * 1024)
|
|
if size_mb > max_mb:
|
|
raise IMessageMediaError(
|
|
f"Attachment size {size_mb:.1f}MB exceeds limit of {max_mb}MB"
|
|
)
|
|
|
|
content_type = resp.headers.get("content-type", "application/octet-stream")
|
|
return data, content_type
|
|
|
|
|
|
async def download_attachment_chunked(
|
|
attachment_guid: str,
|
|
server_url: str,
|
|
password: str,
|
|
chunk_size: int = 1024 * 1024,
|
|
) -> bytes:
|
|
async with httpx.AsyncClient(timeout=httpx.Timeout(300.0)) as client:
|
|
resp = await client.get(
|
|
f"{server_url}/api/v1/attachment/{attachment_guid}/chunk",
|
|
headers={"Password": password},
|
|
)
|
|
resp.raise_for_status()
|
|
return resp.content
|
|
|
|
|
|
def _guess_extension(content_type: str) -> str:
|
|
ext_map = {
|
|
"image/jpeg": ".jpg",
|
|
"image/png": ".png",
|
|
"image/gif": ".gif",
|
|
"image/heic": ".heic",
|
|
"image/heif": ".heif",
|
|
"audio/mpeg": ".mp3",
|
|
"audio/aac": ".aac",
|
|
"audio/wav": ".wav",
|
|
"video/mp4": ".mp4",
|
|
"video/quicktime": ".mov",
|
|
"application/pdf": ".pdf",
|
|
}
|
|
return ext_map.get(content_type, ".bin")
|