Skip to content
Draft
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
1 change: 1 addition & 0 deletions astrbot/api/all.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

# star register
from astrbot.core.star.register import (
register_button_interaction as button_interaction,
register_command as command,
register_command_group as command_group,
register_event_message_type as event_message_type,
Expand Down
6 changes: 6 additions & 0 deletions astrbot/api/event/filter/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from astrbot.core.star.filter.button_interaction import ButtonInteractionFilter
from astrbot.core.star.filter.custom_filter import CustomFilter
from astrbot.core.star.filter.event_message_type import (
EventMessageType,
Expand All @@ -9,6 +10,9 @@
PlatformAdapterTypeFilter,
)
from astrbot.core.star.register import register_after_message_sent as after_message_sent
from astrbot.core.star.register import (
register_button_interaction as button_interaction,
)
from astrbot.core.star.register import register_command as command
from astrbot.core.star.register import register_command_group as command_group
from astrbot.core.star.register import register_custom_filter as custom_filter
Expand Down Expand Up @@ -41,13 +45,15 @@

__all__ = [
"CustomFilter",
"ButtonInteractionFilter",
"EventMessageType",
"EventMessageTypeFilter",
"PermissionType",
"PermissionTypeFilter",
"PlatformAdapterType",
"PlatformAdapterTypeFilter",
"after_message_sent",
"button_interaction",
"command",
"command_group",
"custom_filter",
Expand Down
3 changes: 3 additions & 0 deletions astrbot/core/config/default.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"slack",
"lark",
"line",
"mattermost",
]

# 默认配置
Expand Down Expand Up @@ -540,6 +541,8 @@
"mattermost_url": "https://chat.example.com",
"mattermost_bot_token": "",
"mattermost_reconnect_delay": 5.0,
"unified_webhook_mode": True,
"webhook_uuid": "",
},
# "WebChat": {
# "id": "webchat",
Expand Down
7 changes: 7 additions & 0 deletions astrbot/core/core_lifecycle.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import time
import traceback
from asyncio import Queue
from pathlib import Path

from astrbot.api import logger, sp
from astrbot.core import LogBroker, LogManager
Expand All @@ -27,6 +28,9 @@
from astrbot.core.knowledge_base.kb_mgr import KnowledgeBaseManager
from astrbot.core.persona_mgr import PersonaManager
from astrbot.core.pipeline.scheduler import PipelineContext, PipelineScheduler
from astrbot.core.platform.button_interaction import (
configure_button_callback_registry,
)
from astrbot.core.platform.manager import PlatformManager
from astrbot.core.platform_message_history_mgr import PlatformMessageHistoryManager
from astrbot.core.process_restart import restart_process
Expand Down Expand Up @@ -172,6 +176,9 @@ async def initialize(self) -> None:
LogManager.configure_trace_logger(self.astrbot_config)

await self.db.initialize()
button_callback_db_path = getattr(self.db, "db_path", None)
if isinstance(button_callback_db_path, (str, Path)):
configure_button_callback_registry(button_callback_db_path)
if sp.db_helper is self.db:
await sp.initialize()

Expand Down
116 changes: 114 additions & 2 deletions astrbot/core/message/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,21 @@
import uuid
from enum import Enum
from pathlib import Path, PurePosixPath
from typing import Any, Literal, TypeAlias

from deprecated import deprecated

if sys.version_info >= (3, 14):
from pydantic import BaseModel
from pydantic import BaseModel, Field, StrictBool, StrictFloat, StrictInt, StrictStr
else:
from pydantic.v1 import BaseModel
from pydantic.v1 import (
BaseModel,
Field,
StrictBool,
StrictFloat,
StrictInt,
StrictStr,
)

from astrbot.core import astrbot_config, file_token_service, logger
from astrbot.core.utils.astrbot_path import get_astrbot_temp_path
Expand All @@ -50,6 +58,9 @@ class ComponentType(str, Enum):
Record = "Record" # audio
Video = "Video" # video
File = "File" # file attachment
ActionRow = "ActionRow" # a row of interactive controls
Button = "Button" # an interactive button
ButtonInteraction = "ButtonInteraction" # an inbound button click

# IM-specific Segment Types
Face = "Face" # Emoji segment for Tencent QQ platform
Expand Down Expand Up @@ -124,6 +135,104 @@ async def to_dict(self) -> dict:
return {"type": "text", "data": {"text": self.text}}


JSONValue: TypeAlias = (
StrictStr | StrictInt | StrictFloat | StrictBool | None | list[Any] | dict[str, Any]
)


class ButtonStyle(str, Enum):
"""Portable visual intent for a button."""

DEFAULT = "default"
PRIMARY = "primary"
SUCCESS = "success"
DANGER = "danger"


class CallbackAction(BaseModel):
"""Run bot-side logic when a button is clicked."""

type: Literal["callback"] = "callback"
data: JSONValue | None = None

def __init__(self, **values) -> None:
"""Validate callback context when the component is constructed.

Args:
**values: Pydantic field values for the callback action.

Raises:
ValueError: If data is not valid JSON.
"""
super().__init__(**values)
try:
json.dumps(self.data, allow_nan=False)
except (TypeError, ValueError) as exc:
raise ValueError("Button callback data must be JSON-compatible.") from exc


class UrlAction(BaseModel):
"""Open a URL when a button is clicked."""

type: Literal["url"] = "url"
url: str = Field(min_length=1)


class Button(BaseMessageComponent):
"""A portable interactive button."""

type: ComponentType = ComponentType.Button
id: str = Field(min_length=1)
label: str = Field(min_length=1)
action: CallbackAction | UrlAction
style: ButtonStyle = ButtonStyle.DEFAULT

def toDict(self) -> dict:
"""Serialize the button using the public message component format."""
action = {"type": self.action.type}
if isinstance(self.action, CallbackAction):
if self.action.data is not None:
action["data"] = self.action.data
else:
action["url"] = self.action.url
return {
"type": "button",
"data": {
"id": self.id,
"label": self.label,
"action": action,
"style": self.style.value,
},
}


class ActionRow(BaseMessageComponent):
"""A group of buttons that should be displayed on one row when possible."""

type: ComponentType = ComponentType.ActionRow
buttons: list[Button]
fallback_text: str | None = None

def toDict(self) -> dict:
"""Serialize the row using the public message component format."""
data: dict = {
"buttons": [button.toDict()["data"] for button in self.buttons],
}
if self.fallback_text is not None:
data["fallback_text"] = self.fallback_text
return {"type": "actionrow", "data": data}


class ButtonInteraction(BaseMessageComponent):
"""Normalized inbound event produced by a callback button click."""

type: ComponentType = ComponentType.ButtonInteraction
action_id: str
data: JSONValue | None = None
interaction_id: str
source_message_id: str | None = None


class Face(BaseMessageComponent):
type: ComponentType = ComponentType.Face
id: int
Expand Down Expand Up @@ -924,6 +1033,9 @@ async def to_dict(self):
"record": Record,
"video": Video,
"file": File,
"actionrow": ActionRow,
"button": Button,
"buttoninteraction": ButtonInteraction,
# IM-specific Message Segments
"face": Face,
"at": At,
Expand Down
16 changes: 13 additions & 3 deletions astrbot/core/pipeline/waking_check/stage.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from astrbot.core.message.message_event_result import MessageChain, MessageEventResult
from astrbot.core.platform.astr_message_event import AstrMessageEvent
from astrbot.core.platform.message_type import MessageType
from astrbot.core.star.filter.button_interaction import ButtonInteractionFilter
from astrbot.core.star.filter.command_group import CommandGroupFilter
from astrbot.core.star.filter.permission import PermissionTypeFilter
from astrbot.core.star.session_plugin_manager import SessionPluginManager
Expand Down Expand Up @@ -142,9 +143,13 @@ async def process(
event.is_at_or_wake_command = True
break
# 检查是否是私聊
if event.is_private_chat() and (
not self.friend_message_needs_wake_prefix
or event.get_platform_name() == "webchat"
if (
not event.is_button_interaction()
and event.is_private_chat()
and (
not self.friend_message_needs_wake_prefix
or event.get_platform_name() == "webchat"
)
):
is_wake = True
event.is_wake = True
Expand All @@ -168,6 +173,11 @@ async def process(
EventType.AdapterMessageEvent,
plugins_name=event.plugins_name,
):
if event.is_button_interaction() and not any(
isinstance(handler_filter, ButtonInteractionFilter)
for handler_filter in handler.event_filters
):
continue
if (
self.disable_builtin_commands
and handler.handler_module_path
Expand Down
19 changes: 19 additions & 0 deletions astrbot/core/platform/astr_message_event.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
At,
AtAll,
BaseMessageComponent,
ButtonInteraction,
Face,
Forward,
Image,
Expand Down Expand Up @@ -181,6 +182,24 @@ def get_messages(self) -> list[BaseMessageComponent]:
"""获取消息链。"""
return getattr(self.message_obj, "message", [])

def is_button_interaction(self) -> bool:
"""Return whether this event represents a portable button click."""
return any(
isinstance(component, ButtonInteraction)
for component in self.get_messages()
)

def get_button_interaction(self) -> ButtonInteraction | None:
"""Return the normalized button click carried by this event, if any."""
return next(
(
component
for component in self.get_messages()
if isinstance(component, ButtonInteraction)
),
None,
)

def get_message_type(self) -> MessageType:
"""获取消息类型。"""
message_type = getattr(self.message_obj, "type", None)
Expand Down
Loading
Loading