ForcePilot/backend/package/yuxi/channels/contract/dtos/channel.py
Kris c7eb196a7e chore: 批量完成多模块迭代优化与功能完善
本次提交涵盖了近百处代码优化与功能补全,包括:
1. 完善配置与数据模型:新增expired_at配对记录字段、路由绑定乐观锁版本控制、会话路由信息追踪字段
2. 优化业务流程:添加幂等记录操作人审计、会话合并领域服务文档更新、媒体处理异步化改造
3. 新增功能能力:健康检查时间更新、会话路由信息更新接口、内容审核/幂等记录清理定时任务
4. 修复与简化:移除废弃的max_message_length属性、修复微信iLink适配器配置读取路径、简化配对过期扫描逻辑
5. 代码规范优化:统一敏感词检测工具导入、完善事务上下文处理注释、调整wechat_woc入站适配器sender回退逻辑
2026-07-08 03:57:05 +08:00

747 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""渠道类型 DTO。
定义渠道账户、会话、身份、消息等跨层共享的不可变值对象。所有 DTO 均为
``dataclass(frozen=True)``,仅依赖标准库,禁止泄露领域实体引用。
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from enum import StrEnum
from typing import Any, Literal
from yuxi.channels.contract.dtos.common import BatchOperationFailure, Operator
from yuxi.channels.contract.errors import ValidationError
class ChannelType(str):
"""渠道类型标识符。
由插件 manifest 声明,框架层不做白名单校验。每个插件应使用唯一的
channel_type 值,由 ``PluginRegistry`` 保证唯一性。运行时为 ``str``
子类,可直接作为字典 key、JSON 序列化、FastAPI 参数。
约束:
- 框架层contract / core / application**禁止** 在任何决策语句中
分支到具体 channel_type 值,渠道特性决策 **必须** 通过
``ChannelManifest`` 声明字段驱动。
- 新增渠道仅需在插件 manifest 中声明 channel_type 字符串,无需
修改本类。
- channel_type 值在系统中全局唯一(由 ``PluginRegistry.register``
校验),两个插件不得共用同一 channel_type。
"""
__slots__ = ()
@classmethod
def __get_pydantic_core_schema__(cls, source_type: Any, handler: Any) -> Any:
"""Pydantic 核心 schema以 ``str`` 校验后包装为 ``ChannelType``。
``ChannelType`` 为 ``str`` 子类(非 ``StrEnum``Pydantic 默认
无法为其生成 schema导致 ``ChannelType | None`` 作为 FastAPI
``Query`` / ``Field`` 参数或 Pydantic 模型字段时抛
``PydanticSchemaGenerationError``。本方法以 ``str_schema`` 校验
输入后用 ``cls`` 包装,保留 str 子类的开放值语义(支持
``ChannelType("wechat_ilink")`` 等动态值,无需预定义枚举成员)。
``pydantic_core`` 在方法内延迟导入,避免在 contract 层引入硬依赖,
保持 DTO 模块"仅依赖标准库"的约束(无 Pydantic 环境下本类仍可正常
使用,仅失去 Pydantic 字段类型支持)。
"""
from pydantic_core import core_schema
return core_schema.no_info_after_validator_function(
cls,
core_schema.str_schema(),
)
class AccountStatus(StrEnum):
"""渠道账户状态枚举。
聚合根 ``ChannelAccount`` 的状态机枚举。状态转换规则参见 ``ChannelAccount``
聚合根:
- ``ACTIVE`` ↔ ``DISABLED``:通过 ``enable()`` / ``disable()`` 切换。
- ``ACTIVE`` → ``DEGRADED``:通过 ``degrade()`` 切换(不可从 ``DISABLED`` 降级)。
- ``DEGRADED`` → ``ACTIVE``:通过 ``recover()`` 切换(不可直接 ``enable``)。
- ``DISABLED`` 不可直接 ``recover``,需先 ``enable`` 回到 ``ACTIVE``。
"""
ACTIVE = "active"
DISABLED = "disabled"
DEGRADED = "degraded"
class OnboardingStatus(StrEnum):
"""渠道账号接入态枚举。
描述渠道账号 onboarding 生命周期状态,独立于运行态 ``AccountStatus``
``ACTIVE`` / ``DISABLED`` / ``DEGRADED``onboarding 状态刻画账号
从创建到可用的接入流程进度,运行态刻画已上线账号的实时可用性。两者
解耦后,账号下线(``OFFLINE``)与运行态禁用(``DISABLED``)可独立流转,
避免 onboarding 流程被运行态切换误覆盖。
取值:
PENDING: 账号已创建,凭证未配置。
CONFIGURED: 凭证已落库,未验证连通性。
VERIFIED: 连通性验证通过,未上线。
ONLINE: 已上线(运行态 ``ACTIVE``)。
OFFLINE: 已下线(运行态 ``DISABLED``)。
FAILED: 接入失败(凭证失效 / 验证失败)。
继承 ``StrEnum`` 以兼容 JSON 序列化:``str(OnboardingStatus.PENDING)``
即 ``"pending"``,可直接用于 ``json.dumps`` 与持久化层字符串字段,
无需额外的 ``__str__`` 或 serializer 适配。
"""
PENDING = "pending"
CONFIGURED = "configured"
VERIFIED = "verified"
ONLINE = "online"
OFFLINE = "offline"
FAILED = "failed"
class SessionStatus(StrEnum):
"""渠道会话状态枚举。
标识渠道会话的生命周期状态,用于会话关闭与消息投递决策。继承
``str, Enum`` 以支持 JSON 序列化与字符串比较。
取值:
ACTIVE: 活跃会话,可正常收发消息。
CLOSED: 已关闭会话,停止接收新消息。
"""
ACTIVE = "active"
CLOSED = "closed"
@dataclass(frozen=True)
class ChannelAccount:
"""渠道账户。
描述一个渠道账户的完整配置用于账户管理与消息路由。config 字段为脱敏后
的渠道配置,不包含原始密钥。
字段:
channel_type: 渠道类型。
account_id: 渠道账户 ID全局唯一
display_name: 显示名称。
config: 渠道配置(脱敏后)。
enabled: 是否启用。
status: 账户状态机值active/disabled/degraded
created_at: 创建时间。
updated_at: 更新时间。
transport_cursor: 传输游标Puller类型使用。
last_rotated_at: 凭据最近轮换时间(可选)。
service_user_uid: 关联服务账号 User.uiduser_type='service'),可选。
用于渠道账户以服务账号身份执行内部操作。
onboarding_status: 接入态状态机值pending/configured/verified/
online/offline/failed。描述账号 onboarding 生命周期,与运行态
``status`` 解耦。必填无默认值kw_only
credential_ref: 凭证引用(可选),指向凭证存储中的版本化凭证记录。
credential_version: 凭证版本号(默认 0与 ``credential_ref`` 共同
定位具体凭证版本。
transport_mode: 传输模式(默认 ``both``)。声明账号支持的传输方式:
``pull``(仅拉取)、``stream``(仅流式)、``both``(两者皆可),
供入站/出站链路决策拉取器与流式通道启用策略。
"""
channel_type: ChannelType
account_id: str
display_name: str
config: dict[str, Any]
enabled: bool = True
status: AccountStatus = AccountStatus.ACTIVE
created_at: datetime | None = None
updated_at: datetime | None = None
transport_cursor: str = ""
last_rotated_at: datetime | None = None
service_user_uid: str | None = None
transport_mode: Literal["pull", "stream", "both"] = "both"
onboarding_status: OnboardingStatus = field(kw_only=True)
credential_ref: str | None = field(kw_only=True, default=None)
credential_version: int = field(kw_only=True, default=0)
last_error: str | None = field(kw_only=True, default=None)
plugin_status: str = field(kw_only=True, default="stopped")
version: int = field(kw_only=True, default=1)
def __post_init__(self) -> None:
"""校验必填字段非空。
``channel_type`` 不可为 ``None````account_id`` 与 ``display_name``
必须非空,在构造时即抛出 ``ValidationError``adapter 不再做该校验
INV-8
"""
if self.channel_type is None:
raise ValidationError("channel_type", "must not be None")
if not self.account_id:
raise ValidationError("account_id", "must not be empty")
if not self.display_name:
raise ValidationError("display_name", "must not be empty")
@dataclass(frozen=True)
class ChannelAccountSummary:
"""渠道账户摘要。
用于账户列表查询等场景,仅包含展示必要字段,不泄露配置详情。
字段:
channel_type: 渠道类型。
account_id: 渠道账户 ID。
display_name: 显示名称。
enabled: 是否启用。
status: 账户状态机值active/disabled/degraded
"""
channel_type: ChannelType
account_id: str
display_name: str
enabled: bool
status: AccountStatus = AccountStatus.ACTIVE
@dataclass(frozen=True)
class ChannelSession:
"""渠道会话。
描述渠道侧会话状态,关联会话与统一身份、主会话所有者,支持临时会话标记
与软删除。
字段:
session_id: 会话 ID。
channel_type: 渠道类型。
account_id: 渠道账户 ID。
peer_id: 对端 ID。
chat_type: 会话类型("p2p" | "group")。
conversation_id: 关联的内部会话 ID。
unified_identity_id: 统一身份 ID。
owner_peer_id: 主会话所有者对端 IDFR-26
is_temporary: 临时会话标记FR-27
created_at: 创建时间。
updated_at: 更新时间。
deleted_at: 软删除时间。
closed_at: 会话关闭时间(可选)。
last_message_at: 最近一次消息时间(可选,用于会话列表排序与
inactive 临时会话清理FR-27
last_route_at: 最近路由更新时间可选FR-04/FR-26用于跨渠道
会话排序与路由诊断)。
route_match_source: 最近路由匹配来源(可选,取值见
``ConfigMatchSource`` 枚举,用于路由审计)。
version: 乐观锁版本号(默认 1用于并发更新校验
"""
session_id: str
channel_type: ChannelType
account_id: str
peer_id: str
chat_type: Literal["p2p", "group"]
conversation_id: str | None = None
unified_identity_id: str | None = None
owner_peer_id: str | None = None
is_temporary: bool = False
created_at: datetime | None = None
updated_at: datetime | None = None
deleted_at: datetime | None = None
closed_at: datetime | None = None
last_message_at: datetime | None = None
last_route_at: datetime | None = None
route_match_source: str | None = None
version: int = 1
@dataclass(frozen=True)
class UserIdentity:
"""用户身份。
描述用户在渠道侧或外部身份系统的身份信息,用于身份解析与统一身份关联。
``user_id`` 为可空字段但需显式传入,以区分"未关联用户""使用默认值"
字段:
identity_id: 统一身份 ID。
user_id: 关联用户表 ID可空
identity_type: 身份类型(邮箱 / 手机 / 组织员工 ID
identity_value: 身份值。
channel_type: 渠道类型。
channel_sender_id: 渠道侧发送者 ID。
source: 身份来源。
channel_bindings: 渠道绑定映射 ``{channel_type: [peer_id, ...]}``
记录该身份在各渠道的对端 ID用于跨渠道身份关联与反查。
merged_from: 合并来源 identity_id 列表,记录被合并进本身份的其它身份。
confidence: 身份置信度等级(``low`` / ``medium`` / ``high``
反映身份解析结果的可信程度,用于路由决策与审计。
created_at: 创建时间。
updated_at: 更新时间。
version: 乐观锁版本号P3 渐进式绑定,用于 updateUserIdentity 并发控制)。
"""
identity_id: str
user_id: str | None
identity_type: str
identity_value: str
channel_type: ChannelType | None = None
channel_sender_id: str | None = None
source: str | None = None
channel_bindings: dict[str, list[str]] = field(default_factory=dict)
merged_from: list[str] = field(default_factory=list)
confidence: str = "low"
created_at: datetime | None = None
updated_at: datetime | None = None
version: int = 1
@dataclass(frozen=True)
class SessionInfo:
"""会话信息。
渠道适配器解析原始事件后产出的会话定位信息,用于核心层解析或创建会话。
字段:
channel_type: 渠道类型。
account_id: 渠道账户 ID。
peer_id: 对端 ID。
chat_type: 会话类型("p2p" | "group")。
group_id: 群组 ID。
topic_id: 话题 ID。
"""
channel_type: ChannelType
account_id: str
peer_id: str
chat_type: Literal["p2p", "group"]
group_id: str | None = None
topic_id: str | None = None
@dataclass(frozen=True)
class Message:
"""消息。
描述一条消息的完整状态,包括内部字段与渠道侧状态字段(已读、撤回、编辑
等),用于消息持久化与渠道侧状态同步。
字段:
message_id: 消息 ID。
conversation_id: 会话 ID。
role: 角色user | assistant | admin
content: 消息文本内容。
channel_status: 渠道侧状态FR-09
channel_msg_id: 渠道侧消息 ID。
ref_channel_msg_id: 引用的渠道消息 ID编辑/回复场景)。
channel_status_history: 渠道侧状态事件历史数组FR-09
operations_history: 消息操作历史数组FR-12每项记录一次消息操作
的执行信息(操作类型、执行者、时间戳、是否成功)。
channel_read_at: 渠道侧已读时间。
channel_recalled_at: 渠道侧撤回时间。
channel_edited_at: 渠道侧编辑时间。
created_at: 创建时间。
channel_type: 渠道类型(本期新增,由持久化层从会话关联填充,供 _messageStatus 返回)。
"""
message_id: str
conversation_id: str
role: Literal["user", "assistant", "admin"]
content: str
channel_status: str | None = None
channel_msg_id: str | None = None
ref_channel_msg_id: str | None = None
channel_status_history: tuple[dict, ...] | None = None
operations_history: tuple[dict, ...] | None = None
channel_read_at: datetime | None = None
channel_recalled_at: datetime | None = None
channel_edited_at: datetime | None = None
created_at: datetime | None = None
channel_type: ChannelType | None = None
def __post_init__(self) -> None:
"""校验必填字段非空与角色取值。
``message_id`` 与 ``conversation_id`` 必须非空,``role`` 必须为
``user`` / ``assistant`` / ``admin`` 之一,在构造时即抛出
``ValidationError``adapter 不再做该校验INV-8
"""
if not self.message_id:
raise ValidationError("message_id", "must not be empty")
if not self.conversation_id:
raise ValidationError("conversation_id", "must not be empty")
if not self.content:
raise ValidationError("content", "must not be empty")
if self.role not in ("user", "assistant", "admin"):
raise ValidationError(
"role",
"must be one of: user, assistant, admin",
)
@dataclass(frozen=True)
class RotateCredentialsResult:
"""凭据轮换结果。
描述账户凭据轮换操作的返回结果,包括轮换时间与是否成功撤销旧凭据。
字段:
account_id: 渠道账户 ID。
rotated_at: 轮换完成时间。
old_credentials_revoked: 旧凭据是否已成功撤销。
"""
account_id: str
rotated_at: datetime
old_credentials_revoked: bool
@dataclass(frozen=True)
class ConnectionCheckResult:
"""连接检查结果。
描述单次渠道账户连接连通性检查的结果。
字段:
success: 连接是否成功。
checked_at: 检查时间。
latency_ms: 连接延迟(毫秒,可选)。
error: 错误信息(失败时填充,可选)。
"""
success: bool
checked_at: datetime
latency_ms: int | None = None
error: str | None = None
@dataclass(frozen=True)
class TestConnectionResult:
"""测试连接结果。
描述渠道账户连接测试的完整结果,包括连通性检查与凭据验证两部分。
字段:
account_id: 渠道账户 ID。
connection: 连通性检查结果。
credentials_valid: 凭据是否有效。
tested_at: 测试时间。
"""
account_id: str
connection: ConnectionCheckResult
credentials_valid: bool
tested_at: datetime
@dataclass(frozen=True)
class MessageSearchItem:
"""消息搜索结果项。
描述消息全文搜索的单条匹配结果,包含消息核心字段与匹配高亮信息。
字段:
message_id: 消息 ID。
conversation_id: 会话 ID。
channel_type: 渠道类型。
role: 消息角色user | assistant | admin
content: 消息内容。
created_at: 创建时间。
channel_session_id: 渠道会话 ID可选
channel_account_id: 渠道账户业务 ID可选
peer_id: 对端 ID可选
conversation_title: 会话标题(可选)。
snippet: 匹配片段(高亮后,可选)。
"""
message_id: str
conversation_id: str
channel_type: ChannelType
role: Literal["user", "assistant", "admin"]
content: str
created_at: datetime
channel_session_id: str | None = None
channel_account_id: str | None = None
peer_id: str | None = None
conversation_title: str | None = None
snippet: str | None = None
def __post_init__(self) -> None:
"""校验必填字段非空与角色取值。
``message_id`` / ``conversation_id`` / ``content`` 必须非空,
``channel_type`` 不可为 ``None````role`` 必须为 ``user`` /
``assistant`` / ``admin`` 之一,在构造时即抛出 ``ValidationError``
adapter 不再做该校验INV-8
"""
if not self.message_id:
raise ValidationError("message_id", "must not be empty")
if not self.conversation_id:
raise ValidationError("conversation_id", "must not be empty")
if self.channel_type is None:
raise ValidationError("channel_type", "must not be None")
if not self.content:
raise ValidationError("content", "must not be empty")
if self.role not in ("user", "assistant", "admin"):
raise ValidationError(
"role",
"must be one of: user, assistant, admin",
)
@dataclass(frozen=True)
class WebhookTestCmd:
"""Webhook 测试命令WHK-TEST
由 ``AccountManagementPort.testWebhook`` 引用,触发对指定渠道账户的
webhook 测试事件投递。``account_id`` 缺省时由 dispatch handler 取该
渠道首个账户。
字段:
channel_type: 渠道类型。
operator: 操作人(审计用)。
account_id: 渠道账户 ID可选缺省取首个账户
event_type: 测试事件类型(默认 ``test_event``)。
payload: 测试事件负载(可选,``None`` 归一化为空 dict
"""
channel_type: ChannelType
operator: Operator
account_id: str | None = None
event_type: str = "test_event"
payload: dict[str, Any] | None = None
def __post_init__(self) -> None:
"""校验必填字段非空并归一化 payloadWHK-TEST
``channel_type`` 不可为 ``None````event_type`` 必须为非空字符串;
违规抛 ``ValidationError``,在构造时即拦截,避免空值传播到控制面
管道后才暴露INV-8。``payload`` 为 ``None`` 时归一化为空 dict
与 ``WebhookTestAdapter.testWebhook`` 协议签名(非 Optional对齐
避免适配器解引用 ``None`` 触发 ``TypeError``。
"""
if self.channel_type is None:
raise ValidationError("channel_type", "must not be None")
if not self.event_type:
raise ValidationError("event_type", "must not be empty")
if self.payload is None:
object.__setattr__(self, "payload", {})
@dataclass(frozen=True)
class WebhookTestResult:
"""Webhook 测试结果WHK-TEST
描述测试事件投递的结果,由插件 ``WebhookTestAdapter.testWebhook`` 返回。
字段:
test_id: 测试事件 ID。
delivered: 是否投递成功。
http_status: 渠道侧返回的 HTTP 状态码(投递失败时可能为 None
response_time_ms: 响应延迟(毫秒,投递失败时可能为 None
error: 错误信息(投递失败时填充,可选)。
"""
test_id: str
delivered: bool
http_status: int | None = None
response_time_ms: int | None = None
error: str | None = None
@dataclass(frozen=True)
class BatchStateChangeCmd:
"""批量启停账户命令ACC-BATCH-STATE
由 ``AccountManagementPort.batchEnableAccounts`` /
``batchDisableAccounts`` 引用,支持显式 ID 列表或筛选条件两种模式。
``action`` 取值 ``enable`` / ``disable``,由 dispatch handler 决定。
字段:
action: 操作类型(``enable`` / ``disable``)。
operator: 操作人(审计用)。
account_ids: 显式账户 ID 元组(默认空元组)。
filter: 筛选条件(含 channel_type / status可选
reason: 操作原因(审计用,可选)。
"""
action: Literal["enable", "disable"]
operator: Operator
account_ids: tuple[str, ...] = ()
filter: dict[str, Any] | None = None
reason: str | None = None
def __post_init__(self) -> None:
"""校验 action 取值与至少一个目标模式ACC-BATCH-STATE
``action`` 必须为 ``enable`` / ``disable`` 之一,``account_ids`` 与
``filter`` 至少提供一个,避免无目标的批量操作,在构造时即抛出
``ValidationError``adapter 不再做该校验INV-8
"""
if self.action not in ("enable", "disable"):
raise ValidationError(
"action",
"must be one of: enable, disable",
)
if not self.account_ids and self.filter is None:
raise ValidationError(
"filter",
"at least one of account_ids or filter must be provided",
)
@dataclass(frozen=True)
class BatchStateChangeResult:
"""批量启停账户结果ACC-BATCH-STATE
描述逐条独立事务启停账户的执行结果,``failed`` 使用通用
``BatchOperationFailure````id`` 字段承载 account_id
字段:
total: 待操作账户总数。
succeeded: 成功操作的账户 ID 元组。
failed: 失败条目元组。
"""
total: int
succeeded: tuple[str, ...]
failed: tuple[BatchOperationFailure, ...]
@dataclass(frozen=True)
class AccountExportResult:
"""账户配置导出结果ACC-EXPORT
描述账户配置导出的返回内容,``secrets_included=false`` 时 ``raw_config``
中敏感字段已通过 MaskingPort 脱敏。
字段:
account_id: 渠道账户 ID。
channel_type: 渠道类型。
display_name: 账户显示名。
raw_config: 原始配置(脱敏后或含凭据)。
exported_at: 导出时间。
secrets_included: 是否包含敏感凭据。
"""
account_id: str
channel_type: ChannelType
display_name: str
raw_config: dict[str, Any]
exported_at: datetime
secrets_included: bool
@dataclass(frozen=True)
class CloneAccountCmd:
"""克隆账户命令ACC-CLONE
由 ``AccountManagementPort.cloneAccount`` 引用,基于源账户配置创建
新账户。``include_credentials=false`` 时清除凭据字段。
字段:
channel_type: 渠道类型。
account_id: 源账户 ID。
new_display_name: 新账户显示名(必填)。
operator: 操作人(审计用)。
new_raw_config_overrides: 配置覆盖项(可选,默认空 dict
include_credentials: 是否克隆凭据(默认 False
"""
channel_type: ChannelType
account_id: str
new_display_name: str
operator: Operator
new_raw_config_overrides: dict[str, Any] | None = None
include_credentials: bool = False
def __post_init__(self) -> None:
"""校验必填字段非空ACC-CLONE
``new_display_name`` 必须非空字符串,在构造时即抛出
``ValidationError``,避免空值传播到聚合根 ``clone()`` 后才暴露
INV-8
"""
if not self.new_display_name:
raise ValidationError("new_display_name", "new_display_name must not be empty")
@dataclass(frozen=True)
class CloneAccountResult:
"""克隆账户结果ACC-CLONE
描述克隆操作返回的新账户信息,新账户默认为 DISABLED 状态,需手动 enable。
字段:
new_account_id: 新账户 ID。
cloned_from: 源账户 ID。
status: 新账户状态(默认 DISABLED
created_at: 创建时间。
"""
new_account_id: str
cloned_from: str
status: AccountStatus
created_at: datetime
@dataclass(frozen=True)
class AccountFilter:
"""渠道账户筛选条件值对象ACC-BATCH-STATE
供 ``ChannelAccountRepositoryPort.findAccountsByFilter`` 使用,替换原
``dict[str, Any]`` 弱类型参数。所有字段均可选,但 ``__post_init__``
强制至少提供一个筛选条件,避免全表扫描。
字段:
channel_type: 渠道类型筛选(可选)。
status: 账户状态筛选(可选)。
"""
channel_type: ChannelType | None = None
status: AccountStatus | None = None
def __post_init__(self) -> None:
if self.channel_type is None and self.status is None:
raise ValidationError(
"filter",
"at least one of channel_type or status must be provided",
)
@dataclass(frozen=True)
class SessionFilter:
"""渠道会话筛选条件值对象SES-BATCH-CLOSE-01
供 ``ChannelSessionRepositoryPort.findSessionsByFilter`` 使用,替换原
``dict[str, Any]`` 弱类型参数。所有字段均可选,但 ``__post_init__``
强制至少提供一个筛选条件,避免全表扫描。
字段:
channel_type: 渠道类型筛选(可选)。
unified_identity_id: 统一身份 ID 精确匹配P3 渐进式绑定,
跨渠道会话合并场景,可选)。
inactive_before: 非活跃截止时间;``last_message_at`` 为 NULL
或早于该时间均视为非活跃(可选)。
status: 会话状态筛选(可选)。
"""
channel_type: ChannelType | None = None
unified_identity_id: str | None = None
inactive_before: datetime | None = None
status: SessionStatus | None = None
def __post_init__(self) -> None:
has_filter = any(
field is not None
for field in (
self.channel_type,
self.unified_identity_id,
self.inactive_before,
self.status,
)
)
if not has_filter:
raise ValidationError(
"filter",
"at least one filter field must be provided",
)