123 lines
3.8 KiB
Python
123 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from yuxi.channel.capabilities.levels import (
|
|
EditSupport,
|
|
InteractiveSupport,
|
|
MarkdownSupport,
|
|
MediaSupport,
|
|
StreamingSupport,
|
|
ThreadSupport,
|
|
)
|
|
from yuxi.channel.ports import AuthPort, InboundPort, MetaPort, OutboundPort, SessionPort
|
|
|
|
|
|
@dataclass
|
|
class CapabilityRequirement:
|
|
capability: str
|
|
level: str | None = None
|
|
fallback: str = "ignore"
|
|
|
|
|
|
@dataclass
|
|
class NegotiationResult:
|
|
supported: bool
|
|
fallback: str
|
|
reason: str | None = None
|
|
|
|
|
|
_LEVEL_ORDER = {"none": 0, "basic": 1, "extended": 2, "full": 3}
|
|
|
|
|
|
def supports(channel: MetaPort, capability: str, config: dict | None = None) -> bool:
|
|
try:
|
|
matrix = channel.get_meta().capability_matrix
|
|
except AttributeError:
|
|
return False
|
|
if matrix is None:
|
|
return False
|
|
return matrix.supports(capability, config)
|
|
|
|
|
|
def negotiate(channel: MetaPort, requirement: CapabilityRequirement) -> NegotiationResult:
|
|
if not supports(channel, requirement.capability):
|
|
return NegotiationResult(
|
|
supported=False,
|
|
fallback=requirement.fallback,
|
|
reason=f"Channel does not support '{requirement.capability}'",
|
|
)
|
|
|
|
if requirement.level is not None and not _level_met(channel, requirement):
|
|
return NegotiationResult(
|
|
supported=False,
|
|
fallback=requirement.fallback,
|
|
reason=f"Capability '{requirement.capability}' supported but level '{requirement.level}' not met",
|
|
)
|
|
|
|
return NegotiationResult(supported=True, fallback=requirement.fallback)
|
|
|
|
|
|
def _level_met(channel: MetaPort, requirement: CapabilityRequirement) -> bool:
|
|
matrix = channel.get_meta().capability_matrix
|
|
cap = requirement.capability
|
|
level = requirement.level.lower() if requirement.level else None
|
|
|
|
if cap in {"text", "reactions", "batch_send", "qr_login", "scan_pairing", "pin", "directory"}:
|
|
return level == "enabled"
|
|
|
|
if cap == "markdown":
|
|
return _ordered_level_met(level, matrix.markdown, MarkdownSupport)
|
|
if cap == "interactive":
|
|
return _ordered_level_met(level, matrix.interactive, InteractiveSupport)
|
|
if cap == "threads":
|
|
return _ordered_level_met(level, matrix.threads, ThreadSupport)
|
|
if cap == "edits":
|
|
return _ordered_level_met(level, matrix.edits, EditSupport)
|
|
if cap == "streaming":
|
|
return _ordered_level_met(level, matrix.streaming, StreamingSupport)
|
|
|
|
if cap == "media":
|
|
if level is None:
|
|
return True
|
|
try:
|
|
required = MediaSupport(level.upper())
|
|
except ValueError:
|
|
return False
|
|
return required in matrix.media and required != MediaSupport.NONE
|
|
|
|
return True
|
|
|
|
|
|
def _ordered_level_met(
|
|
level: str | None,
|
|
current: MarkdownSupport | InteractiveSupport | ThreadSupport | EditSupport | StreamingSupport,
|
|
enum_cls: type,
|
|
) -> bool:
|
|
if level is None:
|
|
return True
|
|
required_order = _LEVEL_ORDER.get(level)
|
|
if required_order is None:
|
|
return False
|
|
current_order = _LEVEL_ORDER.get(current.value.lower(), 0)
|
|
return current_order >= required_order and current != enum_cls.NONE
|
|
|
|
|
|
def get_required_ports(capability: str) -> set[type]:
|
|
mapping: dict[str, set[type]] = {
|
|
"text": {OutboundPort},
|
|
"markdown": {OutboundPort},
|
|
"media": {OutboundPort},
|
|
"interactive": {OutboundPort},
|
|
"reactions": {OutboundPort},
|
|
"threads": {OutboundPort, SessionPort},
|
|
"edits": {OutboundPort},
|
|
"streaming": {OutboundPort},
|
|
"batch_send": {OutboundPort},
|
|
"qr_login": {AuthPort},
|
|
"scan_pairing": {InboundPort},
|
|
"pin": {OutboundPort},
|
|
"directory": {OutboundPort},
|
|
}
|
|
return mapping.get(capability, set())
|