该提交实现了完整的抖音开放平台IM渠道插件,包含: 1. 基础配置与账号管理能力 2. 消息去重、安全策略校验 3. 流式回复、多媒体消息发送 4. Webhook回调处理与事件解析 5. 配对认证与流量限流机制
70 lines
2.4 KiB
Python
70 lines
2.4 KiB
Python
import asyncio
|
|
import logging
|
|
|
|
import httpx
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DOWNLOAD_URL = "https://open.douyin.com/api/apps/v1/developer_toolbox/image_material/download/"
|
|
RETRY_MAX = 3
|
|
RETRY_BASE_DELAY = 2
|
|
|
|
|
|
class DouyinMedia:
|
|
def __init__(self, token_provider):
|
|
self._token_provider = token_provider
|
|
self._http: httpx.AsyncClient | None = None
|
|
|
|
async def _client(self) -> httpx.AsyncClient:
|
|
if self._http is None:
|
|
self._http = httpx.AsyncClient(timeout=30.0)
|
|
return self._http
|
|
|
|
async def download(self, media_id: str) -> dict:
|
|
token = self._token_provider()
|
|
if not token:
|
|
return {"success": False, "error": "no access_token"}
|
|
|
|
url = f"{DOWNLOAD_URL}?access_token={token}&media_id={media_id}"
|
|
|
|
for attempt in range(RETRY_MAX):
|
|
try:
|
|
client = await self._client()
|
|
resp = await client.get(url)
|
|
|
|
content_type = resp.headers.get("content-type", "")
|
|
if "application/json" in content_type or resp.text.startswith("{"):
|
|
data = resp.json()
|
|
if data.get("data", {}).get("error_code", -1) != 0:
|
|
logger.warning(
|
|
"Douyin media download failed (attempt %d): error_code=%s",
|
|
attempt + 1,
|
|
data.get("data", {}).get("error_code"),
|
|
)
|
|
await asyncio.sleep(RETRY_BASE_DELAY * (2**attempt))
|
|
continue
|
|
return {"success": False, "error": "unexpected json response"}
|
|
|
|
return {
|
|
"success": True,
|
|
"data": resp.content,
|
|
"content_type": content_type,
|
|
}
|
|
|
|
except Exception as e:
|
|
logger.warning(
|
|
"Douyin media download exception (attempt %d/%d): %s",
|
|
attempt + 1,
|
|
RETRY_MAX,
|
|
e,
|
|
)
|
|
await asyncio.sleep(RETRY_BASE_DELAY * (2**attempt))
|
|
|
|
logger.error("Douyin media download exhausted retries for media_id=%s", media_id)
|
|
return {"success": False, "error": "download retries exhausted"}
|
|
|
|
async def close(self):
|
|
if self._http:
|
|
await self._http.aclose()
|
|
self._http = None
|