Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions astrbot/api/platform/__init__.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,28 @@
from astrbot.core.message.components import *
from astrbot.core.platform import (
ADMIN_MESSAGE_MEMBER_ROLES,
VALID_MESSAGE_MEMBER_ROLES,
AstrBotMessage,
AstrMessageEvent,
Group,
MessageMember,
MessageType,
Platform,
PlatformMetadata,
normalize_message_member_role,
)
from astrbot.core.platform.register import register_platform_adapter

__all__ = [
"ADMIN_MESSAGE_MEMBER_ROLES",
"AstrBotMessage",
"AstrMessageEvent",
"Group",
"MessageMember",
"MessageType",
"Platform",
"PlatformMetadata",
"VALID_MESSAGE_MEMBER_ROLES",
"normalize_message_member_role",
"register_platform_adapter",
]
13 changes: 12 additions & 1 deletion astrbot/core/platform/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
from .astr_message_event import AstrMessageEvent
from .astrbot_message import AstrBotMessage, Group, MessageMember, MessageType
from .astrbot_message import (
ADMIN_MESSAGE_MEMBER_ROLES,
VALID_MESSAGE_MEMBER_ROLES,
AstrBotMessage,
Group,
MessageMember,
MessageType,
normalize_message_member_role,
)
from .platform import Platform
from .platform_metadata import PlatformMetadata

__all__ = [
"ADMIN_MESSAGE_MEMBER_ROLES",
"AstrBotMessage",
"AstrMessageEvent",
"Group",
"MessageMember",
"MessageType",
"Platform",
"PlatformMetadata",
"VALID_MESSAGE_MEMBER_ROLES",
"normalize_message_member_role",
]
16 changes: 12 additions & 4 deletions astrbot/core/platform/astr_message_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@
from astrbot.core.utils.metrics import Metric
from astrbot.core.utils.trace import TraceSpan

from .astrbot_message import AstrBotMessage, Group
from .astrbot_message import (
ADMIN_MESSAGE_MEMBER_ROLES,
AstrBotMessage,
Group,
normalize_message_member_role,
)
from .message_session import MessageSesion, MessageSession # noqa
from .platform_metadata import PlatformMetadata

Expand All @@ -45,8 +50,11 @@ def __init__(
"""消息对象, AstrBotMessage。带有完整的消息结构。"""
self.platform_meta = platform_meta
"""消息平台的信息, 其中 name 是平台的类型,如 aiocqhttp"""
self.role = "member"
"""用户是否是管理员。如果是管理员,这里是 admin"""
sender_role = normalize_message_member_role(
getattr(getattr(message_obj, "sender", None), "role", None)
)
self.role = sender_role or "member"
"""用户在当前上下文中的角色(例如群聊中的管理员/群主/成员)。"""
self.is_wake = False
"""是否唤醒(是否通过 WakingStage)"""
self.is_at_or_wake_command = False
Expand Down Expand Up @@ -238,7 +246,7 @@ def is_wake_up(self) -> bool:

def is_admin(self) -> bool:
"""是否是管理员。"""
return self.role == "admin"
return self.role in ADMIN_MESSAGE_MEMBER_ROLES

async def process_buffer(self, buffer: str, pattern: re.Pattern) -> str:
"""将消息缓冲区中的文本按指定正则表达式分割后发送至消息平台,作为不支持流式输出平台的Fallback。"""
Expand Down
11 changes: 11 additions & 0 deletions astrbot/core/platform/astrbot_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,22 @@

from .message_type import MessageType

VALID_MESSAGE_MEMBER_ROLES = frozenset({"admin", "owner", "member"})
ADMIN_MESSAGE_MEMBER_ROLES = frozenset({"admin", "owner"})


def normalize_message_member_role(role: object) -> str | None:
"""Normalize platform member roles to the supported role set."""
if isinstance(role, str) and role in VALID_MESSAGE_MEMBER_ROLES:
return role
return None


@dataclass
class MessageMember:
user_id: str # 发送者id
nickname: str | None = None
role: str | None = None

def __str__(self) -> str:
# 使用 f-string 来构建返回的字符串表示形式
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
Video,
)
from astrbot.api.platform import Group, MessageMember
from astrbot.core.platform.astrbot_message import normalize_message_member_role


class AiocqhttpMessageEvent(AstrMessageEvent):
Expand Down Expand Up @@ -244,6 +245,7 @@ async def get_group(self, group_id=None, **kwargs):
MessageMember(
user_id=member["user_id"],
nickname=member.get("nickname") or member.get("card"),
role=normalize_message_member_role(member.get("role")),
)
for member in members
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
MessageType,
Platform,
PlatformMetadata,
normalize_message_member_role,
)
from astrbot.core.platform.astr_message_event import MessageSesion

Expand Down Expand Up @@ -208,9 +209,11 @@ async def _convert_handle_message_event(
assert event.sender is not None
abm = AstrBotMessage()
abm.self_id = str(event.self_id)
sender_role = normalize_message_member_role(event.sender.get("role"))
abm.sender = MessageMember(
str(event.sender["user_id"]),
event.sender.get("card") or event.sender.get("nickname", "N/A"),
role=sender_role,
)
if event["message_type"] == "group":
abm.type = MessageType.GROUP_MESSAGE
Expand Down
57 changes: 57 additions & 0 deletions tests/unit/test_aiocqhttp_message_event.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import sys
import types
from unittest.mock import AsyncMock

import pytest

from astrbot.core.platform.astrbot_message import AstrBotMessage, Group, MessageMember
from astrbot.core.platform.message_type import MessageType
from astrbot.core.platform.platform_metadata import PlatformMetadata


@pytest.mark.asyncio
async def test_get_group_normalizes_cached_member_roles():
sys.modules.setdefault(
"astrbot.core.star.star_tools",
types.SimpleNamespace(StarTools=object),
)
from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import (
AiocqhttpMessageEvent,
)

bot = AsyncMock()
bot.call_action.side_effect = [
{"group_name": "Test Group"},
[
{"user_id": "1001", "nickname": "Owner", "role": "owner"},
{"user_id": "1002", "nickname": "Admin", "role": "admin"},
{"user_id": "1003", "nickname": "Guest", "role": "super-admin"},
],
]

message = AstrBotMessage()
message.type = MessageType.GROUP_MESSAGE
message.group = Group(group_id="123456")
message.sender = MessageMember(user_id="1001", nickname="Owner", role="owner")
message.message = []
message.message_str = ""
message.raw_message = None

event = AiocqhttpMessageEvent(
message_str="",
message_obj=message,
platform_meta=PlatformMetadata(
name="aiocqhttp",
description="test",
id="aiocqhttp-test",
),
session_id="123456",
bot=bot,
)

group = await event.get_group()

assert group is not None
assert group.group_owner == "1001"
assert group.group_admins == ["1002"]
assert [member.role for member in group.members] == ["owner", "admin", None]
39 changes: 39 additions & 0 deletions tests/unit/test_astr_message_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,38 @@ def test_init_trace(self, astr_message_event):
assert astr_message_event.span is not None
assert astr_message_event.trace == astr_message_event.span

def test_init_preserves_supported_sender_role(self, platform_meta, astrbot_message):
"""Test supported sender roles are kept during initialization."""
astrbot_message.sender = MessageMember(
user_id="user123",
nickname="TestUser",
role="admin",
)
event = ConcreteAstrMessageEvent(
message_str="Hello world",
message_obj=astrbot_message,
platform_meta=platform_meta,
session_id="session123",
)
assert event.role == "admin"

def test_init_falls_back_to_member_for_invalid_sender_role(
self, platform_meta, astrbot_message
):
"""Test invalid sender roles fall back to member."""
astrbot_message.sender = MessageMember(
user_id="user123",
nickname="TestUser",
role="super-admin",
)
event = ConcreteAstrMessageEvent(
message_str="Hello world",
message_obj=astrbot_message,
platform_meta=platform_meta,
session_id="session123",
)
assert event.role == "member"


class TestUnifiedMsgOrigin:
"""Tests for unified_msg_origin property."""
Expand Down Expand Up @@ -493,6 +525,11 @@ def test_is_admin_when_admin(self, astr_message_event):
astr_message_event.role = "admin"
assert astr_message_event.is_admin() is True

def test_is_admin_when_owner(self, astr_message_event):
"""Test is_admin returns True when role is owner."""
astr_message_event.role = "owner"
assert astr_message_event.is_admin() is True


class TestProcessBuffer:
"""Tests for process_buffer method."""
Expand Down Expand Up @@ -772,10 +809,12 @@ def test_get_sender_fields_without_sender_attr(self, astr_message_event):

def test_get_message_type_with_non_enum_type(self, astr_message_event):
"""get_message_type should handle message_obj.type that is not a MessageType."""

class DummyMessage:
def __init__(self):
self.type = "not_an_enum"
self.message = []

astr_message_event.message_obj = DummyMessage()
message_type = astr_message_event.get_message_type()
assert isinstance(message_type, MessageType)
26 changes: 25 additions & 1 deletion tests/unit/test_astrbot_message.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,50 @@
from unittest.mock import patch

from astrbot.core.message.components import Image, Plain
from astrbot.core.platform.astrbot_message import AstrBotMessage, Group, MessageMember
from astrbot.core.platform.astrbot_message import (
ADMIN_MESSAGE_MEMBER_ROLES,
AstrBotMessage,
Group,
MessageMember,
VALID_MESSAGE_MEMBER_ROLES,
normalize_message_member_role,
)
from astrbot.core.platform.message_type import MessageType


class TestMessageMember:
"""Tests for MessageMember dataclass."""

def test_normalize_message_member_role_accepts_supported_roles(self):
"""Test supported platform roles are preserved."""
for role in VALID_MESSAGE_MEMBER_ROLES:
assert normalize_message_member_role(role) == role

def test_normalize_message_member_role_rejects_invalid_values(self):
"""Test unsupported platform roles are rejected."""
assert normalize_message_member_role("super-admin") is None
assert normalize_message_member_role(123) is None
assert normalize_message_member_role(None) is None

def test_admin_message_member_roles_constant(self):
"""Test admin role constant stays aligned with role semantics."""
assert ADMIN_MESSAGE_MEMBER_ROLES == {"admin", "owner"}

def test_message_member_creation_basic(self):
"""Test creating a MessageMember with required fields."""
member = MessageMember(user_id="user123")

assert member.user_id == "user123"
assert member.nickname is None
assert member.role is None

def test_message_member_creation_with_nickname(self):
"""Test creating a MessageMember with nickname."""
member = MessageMember(user_id="user123", nickname="TestUser")

assert member.user_id == "user123"
assert member.nickname == "TestUser"
assert member.role is None

def test_message_member_str_with_nickname(self):
"""Test __str__ method with nickname."""
Expand Down