From 4dadbf14a38a9fccfdf42da33d910b62e916fcdb Mon Sep 17 00:00:00 2001 From: RockChinQ Date: Sun, 22 Mar 2026 22:32:00 +0800 Subject: [PATCH] feat: EBA unified event system, entities, and adapter base class - Add EBAEvent hierarchy with all unified events (message.*, group.*, friend.*, bot.*, platform.specific) - Add User, UserGroup, UserGroupMember, ChatType, MemberRole entities - Add AbstractPlatformAdapter with optional APIs and capability declaration - Add NotSupportedError for unimplemented optional APIs - All additions are backward-compatible; existing classes unchanged --- .../definition/abstract/platform/adapter.py | 282 +++++++++++--- .../api/entities/builtin/platform/entities.py | 147 ++++++- .../api/entities/builtin/platform/errors.py | 6 + .../api/entities/builtin/platform/events.py | 363 ++++++++++++++++-- 4 files changed, 704 insertions(+), 94 deletions(-) create mode 100644 src/langbot_plugin/api/entities/builtin/platform/errors.py diff --git a/src/langbot_plugin/api/definition/abstract/platform/adapter.py b/src/langbot_plugin/api/definition/abstract/platform/adapter.py index 70c868e..87e94c6 100644 --- a/src/langbot_plugin/api/definition/abstract/platform/adapter.py +++ b/src/langbot_plugin/api/definition/abstract/platform/adapter.py @@ -1,20 +1,22 @@ from __future__ import annotations -# MessageSource的适配器 +# Platform adapter abstract base classes import typing import abc import pydantic import langbot_plugin.api.entities.builtin.platform.message as platform_message import langbot_plugin.api.entities.builtin.platform.events as platform_events +import langbot_plugin.api.entities.builtin.platform.entities as platform_entities import langbot_plugin.api.definition.abstract.platform.event_logger as abstract_platform_logger +from langbot_plugin.api.entities.builtin.platform.errors import NotSupportedError class AbstractMessagePlatformAdapter(pydantic.BaseModel, metaclass=abc.ABCMeta): - """消息平台适配器基类""" + """Message platform adapter base class.""" bot_account_id: str = pydantic.Field(default="") - """机器人账号ID,需要在初始化时设置""" + """Bot account ID, should be set during initialization.""" config: dict @@ -30,12 +32,12 @@ def __init__(self, *args, **kwargs): async def send_message( self, target_type: str, target_id: str, message: platform_message.MessageChain ): - """主动发送消息 + """Send a message proactively. Args: - target_type (str): 目标类型,`person`或`group` - target_id (str): 目标ID - message (platform.types.MessageChain): 消息链 + target_type: Target type, 'person' or 'group'. + target_id: Target ID. + message: Message chain to send. """ raise NotImplementedError @@ -46,12 +48,12 @@ async def reply_message( message: platform_message.MessageChain, quote_origin: bool = False, ): - """回复消息 + """Reply to a message. Args: - message_source (platform.types.MessageEvent): 消息源事件 - message (platform.types.MessageChain): 消息链 - quote_origin (bool, optional): 是否引用原消息. Defaults to False. + message_source: The source message event to reply to. + message: Message chain to send. + quote_origin: Whether to quote the original message. Defaults to False. """ raise NotImplementedError @@ -63,28 +65,30 @@ async def reply_message_chunk( quote_origin: bool = False, is_final: bool = False, ): - """回复消息(流式输出) + """Reply to a message (streaming output). + Args: - message_source (platform.types.MessageEvent): 消息源事件 - message_id (int): 消息ID - message (platform.types.MessageChain): 消息链 - quote_origin (bool, optional): 是否引用原消息. Defaults to False. - is_final (bool, optional): 流式是否结束. Defaults to False. + message_source: The source message event. + bot_message: Bot message context. + message: Message chain to send. + quote_origin: Whether to quote the original message. Defaults to False. + is_final: Whether this is the final chunk. Defaults to False. """ raise NotImplementedError async def create_message_card( self, message_id: typing.Type[str, int], event: platform_events.MessageEvent ) -> bool: - """创建卡片消息 + """Create a card message placeholder for streaming. + Args: - message_id (str): 消息ID - event (platform_events.MessageEvent): 消息源事件 + message_id: Message ID. + event: The source message event. """ return False async def is_muted(self, group_id: int) -> bool: - """获取账号是否在指定群被禁言""" + """Check if the bot is muted in the specified group.""" return False @abc.abstractmethod @@ -95,11 +99,11 @@ def register_listener( [platform_events.Event, AbstractMessagePlatformAdapter], None ], ): - """注册事件监听器 + """Register an event listener. Args: - event_type (typing.Type[platform.types.Event]): 事件类型 - callback (typing.Callable[[platform.types.Event], None]): 回调函数,接收一个参数,为事件 + event_type: The event type to listen for. + callback: Callback function that receives the event and adapter. """ raise NotImplementedError @@ -111,84 +115,272 @@ def unregister_listener( [platform_events.Event, AbstractMessagePlatformAdapter], None ], ): - """注销事件监听器 + """Unregister an event listener. Args: - event_type (typing.Type[platform.types.Event]): 事件类型 - callback (typing.Callable[[platform.types.Event], None]): 回调函数,接收一个参数,为事件 + event_type: The event type to stop listening for. + callback: The callback to remove. """ raise NotImplementedError @abc.abstractmethod async def run_async(self): - """异步运行""" + """Start the adapter asynchronously.""" raise NotImplementedError async def is_stream_output_supported(self) -> bool: - """是否支持流式输出""" + """Check if streaming output is supported.""" return False @abc.abstractmethod async def kill(self) -> bool: - """关闭适配器 + """Shut down the adapter. Returns: - bool: 是否成功关闭,热重载时若此函数返回False则不会重载MessageSource底层 + True if shutdown succeeded. On hot-reload, returning False + prevents the underlying MessageSource from being reloaded. """ raise NotImplementedError +class AbstractPlatformAdapter(AbstractMessagePlatformAdapter): + """Platform adapter base class (EBA version). + + Compared to the legacy AbstractMessagePlatformAdapter: + - Adds universal API methods (edit_message, delete_message, get_group_info, etc.) + - Adds pass-through API (call_platform_api) + - Adds capability declaration (get_supported_events, get_supported_apis) + - Event listeners support all event types, not just message events + """ + + # ---- Capability Declaration ---- + + def get_supported_events(self) -> list[str]: + """Return the list of event types supported by this adapter.""" + return ["message.received"] + + def get_supported_apis(self) -> list[str]: + """Return the list of APIs supported by this adapter.""" + return ["send_message", "reply_message"] + + # ---- Optional Message Methods ---- + + async def edit_message( + self, + chat_type: str, + chat_id: typing.Union[int, str], + message_id: typing.Union[int, str], + new_content: platform_message.MessageChain, + ) -> None: + """Edit a previously sent message.""" + raise NotSupportedError("edit_message") + + async def delete_message( + self, + chat_type: str, + chat_id: typing.Union[int, str], + message_id: typing.Union[int, str], + ) -> None: + """Delete / recall a message.""" + raise NotSupportedError("delete_message") + + async def forward_message( + self, + from_chat_type: str, + from_chat_id: typing.Union[int, str], + message_id: typing.Union[int, str], + to_chat_type: str, + to_chat_id: typing.Union[int, str], + ) -> platform_events.MessageResult: + """Forward a message.""" + raise NotSupportedError("forward_message") + + async def get_message( + self, + chat_type: str, + chat_id: typing.Union[int, str], + message_id: typing.Union[int, str], + ) -> platform_events.MessageReceivedEvent: + """Retrieve a specific message.""" + raise NotSupportedError("get_message") + + # ---- Optional Group Methods ---- + + async def get_group_info( + self, + group_id: typing.Union[int, str], + ) -> platform_entities.UserGroup: + """Get group information.""" + raise NotSupportedError("get_group_info") + + async def get_group_list(self) -> list[platform_entities.UserGroup]: + """Get the list of groups the bot has joined.""" + raise NotSupportedError("get_group_list") + + async def get_group_member_list( + self, + group_id: typing.Union[int, str], + ) -> list[platform_entities.UserGroupMember]: + """Get the member list of a group.""" + raise NotSupportedError("get_group_member_list") + + async def get_group_member_info( + self, + group_id: typing.Union[int, str], + user_id: typing.Union[int, str], + ) -> platform_entities.UserGroupMember: + """Get information about a specific group member.""" + raise NotSupportedError("get_group_member_info") + + async def set_group_name( + self, + group_id: typing.Union[int, str], + name: str, + ) -> None: + """Set the group name.""" + raise NotSupportedError("set_group_name") + + async def mute_member( + self, + group_id: typing.Union[int, str], + user_id: typing.Union[int, str], + duration: int = 0, + ) -> None: + """Mute a group member.""" + raise NotSupportedError("mute_member") + + async def unmute_member( + self, + group_id: typing.Union[int, str], + user_id: typing.Union[int, str], + ) -> None: + """Unmute a group member.""" + raise NotSupportedError("unmute_member") + + async def kick_member( + self, + group_id: typing.Union[int, str], + user_id: typing.Union[int, str], + ) -> None: + """Kick a member from the group.""" + raise NotSupportedError("kick_member") + + async def leave_group( + self, + group_id: typing.Union[int, str], + ) -> None: + """Make the bot leave a group.""" + raise NotSupportedError("leave_group") + + # ---- Optional User Methods ---- + + async def get_user_info( + self, + user_id: typing.Union[int, str], + ) -> platform_entities.User: + """Get user information.""" + raise NotSupportedError("get_user_info") + + async def get_friend_list(self) -> list[platform_entities.User]: + """Get the bot's friend list.""" + raise NotSupportedError("get_friend_list") + + async def approve_friend_request( + self, + request_id: typing.Union[int, str], + approve: bool = True, + remark: typing.Optional[str] = None, + ) -> None: + """Handle a friend request.""" + raise NotSupportedError("approve_friend_request") + + async def approve_group_invite( + self, + request_id: typing.Union[int, str], + approve: bool = True, + ) -> None: + """Handle a group invitation.""" + raise NotSupportedError("approve_group_invite") + + # ---- Optional Media Methods ---- + + async def upload_file( + self, + file_data: bytes, + filename: str, + ) -> str: + """Upload a file. Returns file ID or URL.""" + raise NotSupportedError("upload_file") + + async def get_file_url( + self, + file_id: str, + ) -> str: + """Get a file download URL.""" + raise NotSupportedError("get_file_url") + + # ---- Pass-through API ---- + + async def call_platform_api( + self, + action: str, + params: dict = {}, + ) -> dict: + """Call an adapter-specific platform API.""" + raise NotSupportedError("call_platform_api") + + class AbstractMessageConverter: - """消息链转换器基类""" + """Message chain converter base class.""" @staticmethod def yiri2target(message_chain: platform_message.MessageChain): - """将源平台消息链转换为目标平台消息链 + """Convert internal message chain to platform-specific format. Args: - message_chain (platform.types.MessageChain): 源平台消息链 + message_chain: Internal message chain. Returns: - typing.Any: 目标平台消息链 + Platform-specific message representation. """ raise NotImplementedError @staticmethod def target2yiri(message_chain: typing.Any) -> platform_message.MessageChain: - """将目标平台消息链转换为源平台消息链 + """Convert platform-specific message to internal message chain. Args: - message_chain (typing.Any): 目标平台消息链 + message_chain: Platform-specific message. Returns: - platform.types.MessageChain: 源平台消息链 + Internal message chain. """ raise NotImplementedError class AbstractEventConverter: - """事件转换器基类""" + """Event converter base class.""" @staticmethod def yiri2target(event: typing.Type[platform_events.Event]): - """将源平台事件转换为目标平台事件 + """Convert internal event to platform-specific event. Args: - event (typing.Type[platform.types.Event]): 源平台事件 + event: Internal event. Returns: - typing.Any: 目标平台事件 + Platform-specific event. """ raise NotImplementedError @staticmethod def target2yiri(event: typing.Any) -> platform_events.Event: - """将目标平台事件的调用参数转换为源平台的事件参数对象 + """Convert platform-specific event to internal event. Args: - event (typing.Any): 目标平台事件 + event: Platform-specific event. Returns: - typing.Type[platform.types.Event]: 源平台事件 + Internal event. """ raise NotImplementedError diff --git a/src/langbot_plugin/api/entities/builtin/platform/entities.py b/src/langbot_plugin/api/entities/builtin/platform/entities.py index e4f1c3b..569e2d1 100644 --- a/src/langbot_plugin/api/entities/builtin/platform/entities.py +++ b/src/langbot_plugin/api/entities/builtin/platform/entities.py @@ -1,3 +1,8 @@ +# -*- coding: utf-8 -*- +""" +Platform entity models. +""" + import abc from datetime import datetime from enum import Enum @@ -7,71 +12,171 @@ class Entity(pydantic.BaseModel): - """实体,表示一个用户或群。""" + """Base entity representing a user or group.""" id: typing.Union[int, str] - """ID。""" + """Entity ID.""" @abc.abstractmethod def get_name(self) -> str: - """名称。""" + """Get display name.""" + + +############################### +# EBA entities (backward-compatible additions) +############################### + +class ChatType(str, Enum): + """Chat/session type.""" + + PRIVATE = "private" + """Private (direct) chat.""" + GROUP = "group" + """Group chat.""" + + +class MemberRole(str, Enum): + """Group member role.""" + + OWNER = "owner" + """Group owner.""" + ADMIN = "admin" + """Administrator.""" + MEMBER = "member" + """Regular member.""" + + +class User(pydantic.BaseModel): + """Unified user entity. + + Provides a common representation for Friend / GroupMember basics. + """ + + id: typing.Union[int, str] + """User ID.""" + + nickname: str = "" + """Display name / nickname.""" + + avatar_url: typing.Optional[str] = None + """Avatar URL.""" + + is_bot: bool = False + """Whether this user is a bot.""" + + username: typing.Optional[str] = None + """Platform username (e.g. Telegram @username).""" + + remark: typing.Optional[str] = None + """Remark / alias set by the current user.""" + + +class UserGroup(pydantic.BaseModel): + """Group entity (EBA version). + + Coexists with the legacy Group class; named UserGroup to avoid conflicts. + """ + + id: typing.Union[int, str] + """Group ID.""" + + name: str = "" + """Group name.""" + + description: typing.Optional[str] = None + """Group description.""" + + member_count: typing.Optional[int] = None + """Number of members.""" + + avatar_url: typing.Optional[str] = None + """Group avatar URL.""" + + owner_id: typing.Optional[typing.Union[int, str]] = None + """Owner's user ID.""" + + +class UserGroupMember(pydantic.BaseModel): + """Group member entity (EBA version).""" + + user: User + """User information.""" + + group_id: typing.Union[int, str] + """ID of the group this member belongs to.""" + + role: MemberRole = MemberRole.MEMBER + """Role within the group.""" + + display_name: typing.Optional[str] = None + """Display name within the group.""" + + joined_at: typing.Optional[float] = None + """Timestamp when the user joined the group.""" + + title: typing.Optional[str] = None + """Special title / badge within the group.""" + +############################### +# Legacy entities (unchanged) +############################### class Friend(Entity): - """私聊对象。""" + """Friend (direct-chat peer).""" id: typing.Union[int, str] - """ID。""" + """ID.""" nickname: typing.Optional[str] - """昵称。""" + """Nickname.""" remark: typing.Optional[str] - """备注。""" + """Remark.""" def get_name(self) -> str: return self.nickname or self.remark or "" class Permission(str, Enum): - """群成员身份权限。""" + """Group member permission level.""" Member = "MEMBER" - """成员。""" + """Regular member.""" Administrator = "ADMINISTRATOR" - """管理员。""" + """Administrator.""" Owner = "OWNER" - """群主。""" + """Group owner.""" def __repr__(self) -> str: return repr(self.value) class Group(Entity): - """群。""" + """Group.""" id: typing.Union[int, str] - """群号。""" + """Group ID.""" name: str - """群名称。""" + """Group name.""" permission: Permission - """Bot 在群中的权限。""" + """Bot's permission level in this group.""" def get_name(self) -> str: return self.name class GroupMember(Entity): - """群成员。""" + """Group member.""" id: typing.Union[int, str] - """群员 ID。""" + """Member ID.""" member_name: str - """群员名称。""" + """Member display name.""" permission: Permission - """在群中的权限。""" + """Permission level in the group.""" group: Group - """群。""" + """The group this member belongs to.""" special_title: str = "" - """群头衔。""" + """Special title within the group.""" def get_name(self) -> str: return self.member_name diff --git a/src/langbot_plugin/api/entities/builtin/platform/errors.py b/src/langbot_plugin/api/entities/builtin/platform/errors.py new file mode 100644 index 0000000..d7b7e99 --- /dev/null +++ b/src/langbot_plugin/api/entities/builtin/platform/errors.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +class NotSupportedError(Exception): + def __init__(self, api_name: str, *args): + super().__init__(f"API '{api_name}' is not supported by this adapter", *args) + self.api_name = api_name diff --git a/src/langbot_plugin/api/entities/builtin/platform/events.py b/src/langbot_plugin/api/entities/builtin/platform/events.py index d91320a..4680cc4 100644 --- a/src/langbot_plugin/api/entities/builtin/platform/events.py +++ b/src/langbot_plugin/api/entities/builtin/platform/events.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -此模块提供事件模型。 +Platform event models. """ import typing @@ -12,14 +12,14 @@ class Event(pydantic.BaseModel): - """事件基类。 + """Base event class. Args: - type: 事件名。 + type: Event type name. """ type: str - """事件名。""" + """Event type name.""" def __repr__(self): return ( @@ -51,44 +51,46 @@ def get_subtype(cls, name: str) -> typing.Type["Event"]: ############################### -# Message Event +# Legacy Message Events (unchanged) +############################### + class MessageEvent(Event): - """消息事件。 + """Message event. Args: - type: 事件名。 - message_chain: 消息内容。 + type: Event type name. + message_chain: Message content. """ type: str - """事件名。""" + """Event type name.""" message_chain: platform_message.MessageChain - """消息内容。""" + """Message content.""" time: float | None = None - """消息发送时间戳。""" + """Message timestamp.""" source_platform_object: typing.Optional[typing.Any] = None - """原消息平台对象。 - 供消息平台适配器开发者使用,如果回复用户时需要使用原消息事件对象的信息, - 那么可以将其存到这个字段以供之后取出使用。""" + """Original platform event object. + For adapter developers: store the raw platform event here so it can be + retrieved later when replying to the user.""" class FriendMessage(MessageEvent): - """私聊消息。 + """Private (direct) message. Args: - type: 事件名。 - sender: 发送消息的好友。 - message_chain: 消息内容。 + type: Event type name. + sender: The friend who sent the message. + message_chain: Message content. """ type: str = "FriendMessage" - """事件名。""" + """Event type name.""" sender: platform_entities.Friend - """发送消息的好友。""" + """Message sender.""" message_chain: platform_message.MessageChain - """消息内容。""" + """Message content.""" def model_dump(self, **kwargs): return { @@ -100,20 +102,20 @@ def model_dump(self, **kwargs): class GroupMessage(MessageEvent): - """群消息。 + """Group message. Args: - type: 事件名。 - sender: 发送消息的群成员。 - message_chain: 消息内容。 + type: Event type name. + sender: The group member who sent the message. + message_chain: Message content. """ type: str = "GroupMessage" - """事件名。""" + """Event type name.""" sender: platform_entities.GroupMember - """发送消息的群成员。""" + """Message sender.""" message_chain: platform_message.MessageChain - """消息内容。""" + """Message content.""" @property def group(self) -> platform_entities.Group: @@ -126,3 +128,308 @@ def model_dump(self, **kwargs): "message_chain": self.message_chain.model_dump(), "time": self.time, } + + +############################### +# EBA Unified Event System (new) +############################### + +class EBAEvent(Event): + """EBA event base class. + + All unified EBA events inherit from this class. + Coexists with the legacy MessageEvent hierarchy. + """ + + type: str + """Event type identifier, e.g. 'message.received'.""" + + timestamp: float = 0.0 + """Event timestamp.""" + + bot_uuid: str = "" + """UUID of the bot that received this event.""" + + adapter_name: str = "" + """Name of the adapter that produced this event.""" + + source_platform_object: typing.Optional[typing.Any] = None + """Raw platform event object for internal adapter use.""" + + +# ---- Message Events ---- + +class MessageReceivedEvent(EBAEvent): + """New message received. Replaces legacy FriendMessage / GroupMessage.""" + + type: str = "message.received" + + message_id: typing.Union[int, str] = "" + """Message ID.""" + + message_chain: platform_message.MessageChain = platform_message.MessageChain([]) + """Message content.""" + + sender: platform_entities.User = pydantic.Field(default_factory=lambda: platform_entities.User(id="")) + """Message sender.""" + + chat_type: platform_entities.ChatType = platform_entities.ChatType.PRIVATE + """Chat type.""" + + chat_id: typing.Union[int, str] = "" + """Chat ID (user ID for private chats, group ID for group chats).""" + + group: typing.Optional[platform_entities.UserGroup] = None + """Group info (only present in group chats).""" + + def to_legacy_event(self) -> typing.Union[FriendMessage, GroupMessage]: + """Convert this EBA event to a legacy-format event (compatibility layer).""" + if self.chat_type == platform_entities.ChatType.PRIVATE: + return FriendMessage( + sender=platform_entities.Friend( + id=self.sender.id, + nickname=self.sender.nickname, + remark=self.sender.remark, + ), + message_chain=self.message_chain, + time=self.timestamp, + source_platform_object=self.source_platform_object, + ) + else: + group = platform_entities.Group( + id=self.group.id if self.group else self.chat_id, + name=self.group.name if self.group else "", + permission=platform_entities.Permission.Member, + ) + return GroupMessage( + sender=platform_entities.GroupMember( + id=self.sender.id, + member_name=self.sender.nickname, + permission=platform_entities.Permission.Member, + group=group, + ), + message_chain=self.message_chain, + time=self.timestamp, + source_platform_object=self.source_platform_object, + ) + + +class MessageEditedEvent(EBAEvent): + """Message was edited.""" + + type: str = "message.edited" + + message_id: typing.Union[int, str] = "" + """ID of the edited message.""" + + new_content: platform_message.MessageChain = platform_message.MessageChain([]) + """New content after editing.""" + + editor: platform_entities.User = pydantic.Field(default_factory=lambda: platform_entities.User(id="")) + """User who edited the message.""" + + chat_type: platform_entities.ChatType = platform_entities.ChatType.PRIVATE + chat_id: typing.Union[int, str] = "" + group: typing.Optional[platform_entities.UserGroup] = None + + +class MessageDeletedEvent(EBAEvent): + """Message was deleted / recalled.""" + + type: str = "message.deleted" + + message_id: typing.Union[int, str] = "" + """ID of the deleted message.""" + + operator: typing.Optional[platform_entities.User] = None + """User who deleted the message.""" + + chat_type: platform_entities.ChatType = platform_entities.ChatType.PRIVATE + chat_id: typing.Union[int, str] = "" + group: typing.Optional[platform_entities.UserGroup] = None + + +class MessageReactionEvent(EBAEvent): + """Message received an emoji reaction.""" + + type: str = "message.reaction" + + message_id: typing.Union[int, str] = "" + """ID of the reacted message.""" + + user: platform_entities.User = pydantic.Field(default_factory=lambda: platform_entities.User(id="")) + """User who reacted.""" + + reaction: str = "" + """Reaction emoji identifier.""" + + is_add: bool = True + """True if reaction was added, False if removed.""" + + chat_type: platform_entities.ChatType = platform_entities.ChatType.PRIVATE + chat_id: typing.Union[int, str] = "" + group: typing.Optional[platform_entities.UserGroup] = None + + +# ---- Group Events ---- + +class MemberJoinedEvent(EBAEvent): + """New member joined a group.""" + + type: str = "group.member_joined" + + group: platform_entities.UserGroup = pydantic.Field(default_factory=lambda: platform_entities.UserGroup(id="")) + """The group.""" + + member: platform_entities.User = pydantic.Field(default_factory=lambda: platform_entities.User(id="")) + """The member who joined.""" + + inviter: typing.Optional[platform_entities.User] = None + """Inviter (if applicable).""" + + join_type: typing.Optional[str] = None + """How the member joined: 'invite' / 'request' / 'direct' / None.""" + + +class MemberLeftEvent(EBAEvent): + """Member left a group.""" + + type: str = "group.member_left" + + group: platform_entities.UserGroup = pydantic.Field(default_factory=lambda: platform_entities.UserGroup(id="")) + member: platform_entities.User = pydantic.Field(default_factory=lambda: platform_entities.User(id="")) + + is_kicked: bool = False + """Whether the member was kicked.""" + + operator: typing.Optional[platform_entities.User] = None + """Operator (the admin who kicked, if applicable).""" + + +class MemberBannedEvent(EBAEvent): + """Member was muted / restricted.""" + + type: str = "group.member_banned" + + group: platform_entities.UserGroup = pydantic.Field(default_factory=lambda: platform_entities.UserGroup(id="")) + member: platform_entities.User = pydantic.Field(default_factory=lambda: platform_entities.User(id="")) + operator: typing.Optional[platform_entities.User] = None + duration: typing.Optional[int] = None + """Mute duration in seconds. None means permanent.""" + + +class GroupInfoUpdatedEvent(EBAEvent): + """Group info was updated.""" + + type: str = "group.info_updated" + + group: platform_entities.UserGroup = pydantic.Field(default_factory=lambda: platform_entities.UserGroup(id="")) + """Updated group info.""" + + operator: typing.Optional[platform_entities.User] = None + changed_fields: list[str] = [] + """List of field names that changed.""" + + +# ---- Friend Events ---- + +class FriendRequestReceivedEvent(EBAEvent): + """Friend request received.""" + + type: str = "friend.request_received" + + request_id: typing.Union[int, str] = "" + """Request ID.""" + + user: platform_entities.User = pydantic.Field(default_factory=lambda: platform_entities.User(id="")) + """The user who sent the request.""" + + message: typing.Optional[str] = None + """Verification message.""" + + +class FriendAddedEvent(EBAEvent): + """Friend successfully added.""" + + type: str = "friend.added" + + user: platform_entities.User = pydantic.Field(default_factory=lambda: platform_entities.User(id="")) + + +class FriendRemovedEvent(EBAEvent): + """Friend was removed.""" + + type: str = "friend.removed" + + user: platform_entities.User = pydantic.Field(default_factory=lambda: platform_entities.User(id="")) + + +# ---- Bot Status Events ---- + +class BotInvitedToGroupEvent(EBAEvent): + """Bot was invited to join a group.""" + + type: str = "bot.invited_to_group" + + group: platform_entities.UserGroup = pydantic.Field(default_factory=lambda: platform_entities.UserGroup(id="")) + inviter: typing.Optional[platform_entities.User] = None + + request_id: typing.Optional[typing.Union[int, str]] = None + """Invitation request ID.""" + + +class BotRemovedFromGroupEvent(EBAEvent): + """Bot was removed from a group.""" + + type: str = "bot.removed_from_group" + + group: platform_entities.UserGroup = pydantic.Field(default_factory=lambda: platform_entities.UserGroup(id="")) + operator: typing.Optional[platform_entities.User] = None + + +class BotMutedEvent(EBAEvent): + """Bot was muted in a group.""" + + type: str = "bot.muted" + + group: platform_entities.UserGroup = pydantic.Field(default_factory=lambda: platform_entities.UserGroup(id="")) + operator: typing.Optional[platform_entities.User] = None + duration: typing.Optional[int] = None + + +class BotUnmutedEvent(EBAEvent): + """Bot was unmuted in a group.""" + + type: str = "bot.unmuted" + + group: platform_entities.UserGroup = pydantic.Field(default_factory=lambda: platform_entities.UserGroup(id="")) + operator: typing.Optional[platform_entities.User] = None + + +# ---- Platform-Specific Events ---- + +class PlatformSpecificEvent(EBAEvent): + """Platform-specific event. + + Used when the adapter cannot map an event to a standard type. + """ + + type: str = "platform.specific" + + action: str = "" + """Platform-specific action identifier.""" + + data: dict = {} + """Event data; structure defined by each adapter.""" + + +# ---- Message Send Result ---- + +class MessageResult(pydantic.BaseModel): + """Result of a message send operation.""" + + message_id: typing.Optional[typing.Union[int, str]] = None + """Message ID after successful send.""" + + raw: typing.Optional[dict] = None + """Raw platform response data."""