新增 IRC 渠道的完整扩展实现,包括客户端连接管理、消息收发与流式处理、安全策略与配对验证、消息去重与规范化、状态监控与健康探测、配置模型与账户管理、错误处理等模块。
93 lines
3.1 KiB
Python
93 lines
3.1 KiB
Python
import logging
|
|
from enum import Enum
|
|
|
|
from yuxi.channel.extensions.irc.client import IRCFatalError
|
|
from yuxi.channel.protocols import ClassifiedError, ErrorSeverity
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class IrcErrorType(str, Enum):
|
|
AUTH = "auth"
|
|
NETWORK = "network"
|
|
CHANNEL = "channel"
|
|
NICK = "nick"
|
|
RATE_LIMIT = "rate_limit"
|
|
PERMISSION = "permission"
|
|
TARGET = "target"
|
|
UNKNOWN = "unknown"
|
|
|
|
|
|
NUMERIC_ERROR_MAP: dict[str, tuple[IrcErrorType, str]] = {
|
|
"401": (IrcErrorType.TARGET, "No such nick/channel"),
|
|
"403": (IrcErrorType.TARGET, "No such channel"),
|
|
"404": (IrcErrorType.PERMISSION, "Cannot send to channel"),
|
|
"405": (IrcErrorType.CHANNEL, "Too many channels"),
|
|
"432": (IrcErrorType.NICK, "Erroneous nickname"),
|
|
"433": (IrcErrorType.NICK, "Nickname in use"),
|
|
"436": (IrcErrorType.NICK, "Nick collision"),
|
|
"464": (IrcErrorType.AUTH, "Password mismatch"),
|
|
"465": (IrcErrorType.AUTH, "You are banned"),
|
|
"471": (IrcErrorType.CHANNEL, "Channel is full"),
|
|
"473": (IrcErrorType.CHANNEL, "Invite only channel"),
|
|
"474": (IrcErrorType.PERMISSION, "Banned from channel"),
|
|
"475": (IrcErrorType.CHANNEL, "Bad channel key"),
|
|
"482": (IrcErrorType.PERMISSION, "ChanOp privileges needed"),
|
|
}
|
|
|
|
|
|
def classify_irc_error(numeric: str) -> tuple[IrcErrorType, str]:
|
|
return NUMERIC_ERROR_MAP.get(numeric, (IrcErrorType.UNKNOWN, f"Unknown error ({numeric})"))
|
|
|
|
|
|
_DEFAULT_RETRY_AFTER_MS = 2000
|
|
|
|
_IRC_NUMERIC_TO_SEVERITY: dict[str, ErrorSeverity] = {
|
|
IrcErrorType.AUTH: ErrorSeverity.FATAL,
|
|
IrcErrorType.NETWORK: ErrorSeverity.NETWORK,
|
|
IrcErrorType.NICK: ErrorSeverity.FATAL,
|
|
IrcErrorType.RATE_LIMIT: ErrorSeverity.RATE_LIMITED,
|
|
IrcErrorType.PERMISSION: ErrorSeverity.FORBIDDEN,
|
|
IrcErrorType.TARGET: ErrorSeverity.FATAL,
|
|
IrcErrorType.CHANNEL: ErrorSeverity.FATAL,
|
|
IrcErrorType.UNKNOWN: ErrorSeverity.FATAL,
|
|
}
|
|
|
|
|
|
def classify_error(error: BaseException) -> ClassifiedError:
|
|
if isinstance(error, IRCFatalError):
|
|
msg = str(error).upper()
|
|
for numeric, (error_type, _) in NUMERIC_ERROR_MAP.items():
|
|
if numeric in msg:
|
|
severity = _IRC_NUMERIC_TO_SEVERITY.get(error_type, ErrorSeverity.FATAL)
|
|
return ClassifiedError(
|
|
severity=severity,
|
|
original_error=error,
|
|
error_message=str(error),
|
|
)
|
|
return ClassifiedError(
|
|
severity=ErrorSeverity.FATAL,
|
|
original_error=error,
|
|
error_message=str(error),
|
|
)
|
|
|
|
if isinstance(error, (ConnectionError, OSError)):
|
|
return ClassifiedError(
|
|
severity=ErrorSeverity.NETWORK,
|
|
original_error=error,
|
|
error_message=str(error),
|
|
)
|
|
|
|
if isinstance(error, TimeoutError):
|
|
return ClassifiedError(
|
|
severity=ErrorSeverity.NETWORK,
|
|
original_error=error,
|
|
error_message=str(error),
|
|
)
|
|
|
|
return ClassifiedError(
|
|
severity=ErrorSeverity.RETRYABLE,
|
|
original_error=error,
|
|
error_message=str(error),
|
|
)
|