新增 Twitter 和 Viber 两个渠道扩展。 Twitter 渠道扩展功能模块: - auth: OAuth 认证管理 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - session: 会话管理 - tweets: 推文管理 - social: 社交互动 - reactions: 表情反应 - media: 媒体资源处理 Viber 渠道扩展功能模块: - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - pairing: 用户配对与绑定 - security: 安全校验 - dedupe: 消息去重 - monitor: 渠道状态监控 - status: 会话状态管理 - rate_limiter: 速率限制 - media: 媒体资源处理
86 lines
2.6 KiB
Python
86 lines
2.6 KiB
Python
from __future__ import annotations
|
|
|
|
from enum import StrEnum
|
|
|
|
|
|
class TwitterErrorKind(StrEnum):
|
|
RETRYABLE = "retryable"
|
|
RATE_LIMITED = "rate_limited"
|
|
AUTH = "auth"
|
|
NOT_AUTHORIZED_DM = "not_authorized_dm"
|
|
DUPLICATE = "duplicate"
|
|
MESSAGE_TOO_LONG = "message_too_long"
|
|
MEDIA_TOO_LARGE = "media_too_large"
|
|
MEDIA_INVALID = "media_invalid"
|
|
ACCOUNT_LOCKED = "account_locked"
|
|
FORBIDDEN = "forbidden"
|
|
NOT_FOUND = "not_found"
|
|
FATAL = "fatal"
|
|
NETWORK = "network"
|
|
|
|
|
|
def classify_error(
|
|
status_code: int | None,
|
|
error_body: dict | str | None,
|
|
) -> tuple[TwitterErrorKind, str, float | None]:
|
|
description = ""
|
|
if isinstance(error_body, dict):
|
|
description = error_body.get("detail", error_body.get("title", ""))
|
|
errors_list = error_body.get("errors", [])
|
|
if errors_list:
|
|
description = errors_list[0].get("message", description)
|
|
elif isinstance(error_body, str):
|
|
description = error_body
|
|
|
|
retry_after = None
|
|
if isinstance(error_body, dict):
|
|
retry_after = error_body.get("retry_after")
|
|
|
|
if status_code == 429:
|
|
return (
|
|
TwitterErrorKind.RATE_LIMITED,
|
|
description,
|
|
float(retry_after) if retry_after else 180.0,
|
|
)
|
|
|
|
if status_code == 401:
|
|
return TwitterErrorKind.AUTH, description, None
|
|
|
|
if status_code == 403:
|
|
if "not authorized to send" in description.lower():
|
|
return TwitterErrorKind.NOT_AUTHORIZED_DM, description, None
|
|
if "duplicate" in description.lower():
|
|
return TwitterErrorKind.DUPLICATE, description, None
|
|
return TwitterErrorKind.FORBIDDEN, description, None
|
|
|
|
if status_code == 400:
|
|
if "too long" in description.lower() or "length" in description.lower():
|
|
return TwitterErrorKind.MESSAGE_TOO_LONG, description, None
|
|
if "media" in description.lower():
|
|
return TwitterErrorKind.MEDIA_INVALID, description, None
|
|
if "duplicate" in description.lower():
|
|
return TwitterErrorKind.DUPLICATE, description, None
|
|
return TwitterErrorKind.FATAL, description, None
|
|
|
|
if status_code == 404:
|
|
return TwitterErrorKind.NOT_FOUND, description, None
|
|
|
|
if status_code and status_code >= 500:
|
|
return TwitterErrorKind.RETRYABLE, description, None
|
|
|
|
if status_code is None:
|
|
return TwitterErrorKind.NETWORK, description, None
|
|
|
|
return TwitterErrorKind.FATAL, description, None
|
|
|
|
|
|
def is_retryable(kind: TwitterErrorKind) -> bool:
|
|
return kind in (
|
|
TwitterErrorKind.RETRYABLE,
|
|
TwitterErrorKind.RATE_LIMITED,
|
|
TwitterErrorKind.NETWORK,
|
|
)
|
|
|
|
|
|
MAX_RETRIES = 3
|