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 src/nonebot_plugin_alconna/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@
from .uniseg import AtAll as AtAll
from .uniseg import Audio as Audio
from .uniseg import Button as Button
from .uniseg import ButtonCallback as ButtonCallback
from .uniseg import ButtonInteraction as ButtonInteraction
from .uniseg import CustomNode as CustomNode
from .uniseg import Emoji as Emoji
from .uniseg import FallbackStrategy as FallbackStrategy
Expand All @@ -120,6 +122,7 @@
from .uniseg import Text as Text
from .uniseg import UniMessage as UniMessage
from .uniseg import UniMsg as UniMsg
from .uniseg import UniversalButtonInteraction as UniversalButtonInteraction
from .uniseg import UniversalMessage as UniversalMessage
from .uniseg import UniversalSegment as UniversalSegment
from .uniseg import Video as Video
Expand All @@ -129,15 +132,18 @@
from .uniseg import apply_media_to_url as apply_media_to_url
from .uniseg import at_in as at_in
from .uniseg import at_me as at_me
from .uniseg import button_rule as button_rule
from .uniseg import custom_handler as custom_handler
from .uniseg import custom_register as custom_register
from .uniseg import get_bot as get_bot
from .uniseg import get_button_interaction as get_button_interaction
from .uniseg import get_message_id as get_message_id
from .uniseg import get_target as get_target
from .uniseg import image_fetch as image_fetch
from .uniseg import message_edit as message_edit
from .uniseg import message_reaction as message_reaction
from .uniseg import message_recall as message_recall
from .uniseg import on_button as on_button
from .uniseg import patch_matcher_send as patch_matcher_send
from .uniseg import patch_saa as patch_saa

Expand Down
6 changes: 6 additions & 0 deletions src/nonebot_plugin_alconna/uniseg/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,19 @@
from .functions import message_edit as message_edit
from .functions import message_reaction as message_reaction
from .functions import message_recall as message_recall
from .interaction import ButtonInteraction as ButtonInteraction
from .interaction import button_rule as button_rule
from .interaction import get_button_interaction as get_button_interaction
from .interaction import on_button as on_button
from .message import UniMessage as UniMessage
from .params import ButtonCallback as ButtonCallback
from .params import MessageId as MessageId
from .params import MessageTarget as MessageTarget
from .params import MsgId as MsgId
from .params import MsgTarget as MsgTarget
from .params import OriginalUniMsg as OriginalUniMsg
from .params import UniMsg as UniMsg
from .params import UniversalButtonInteraction as UniversalButtonInteraction
from .params import UniversalMessage as UniversalMessage
from .params import UniversalSegment as UniversalSegment
from .receipt import Receipt as Receipt
Expand Down
15 changes: 15 additions & 0 deletions src/nonebot_plugin_alconna/uniseg/adapters/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
if TYPE_CHECKING:
from ..builder import MessageBuilder
from ..exporter import MessageExporter
from ..interaction import ButtonInteractionAdapter
from ..loader import BaseLoader
from ..target import TargetFetcher

Expand Down Expand Up @@ -49,6 +50,7 @@
SupportAdapter.nonebug.value: loaders[SupportAdapter.nonebug.value].get_builder()
}
FETCHER_MAPPING: dict[str, "TargetFetcher"] = {}
INTERACTION_MAPPING: dict[str, "ButtonInteractionAdapter | None"] = {}
adapters = {}
try:
adapters = get_adapters()
Expand Down Expand Up @@ -146,3 +148,16 @@ def alter_get_fetcher(adapter_name: str):
6,
)
return None


def alter_get_interaction(adapter_name: str):
if adapter_name in INTERACTION_MAPPING:
return INTERACTION_MAPPING[adapter_name]
if adapter_name not in loaders:
return None
try:
interaction = loaders[adapter_name].get_interaction()
except NotImplementedError:
interaction = None
INTERACTION_MAPPING[adapter_name] = interaction
return interaction
5 changes: 5 additions & 0 deletions src/nonebot_plugin_alconna/uniseg/adapters/qq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,8 @@ def get_fetcher(self):
from .target import QQTargetFetcher

return QQTargetFetcher()

def get_interaction(self):
from .interaction import QQButtonInteractionAdapter

return QQButtonInteractionAdapter()
61 changes: 48 additions & 13 deletions src/nonebot_plugin_alconna/uniseg/adapters/qq/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ def get_target(self, event: Event, bot: Bot | None = None) -> Target:
if event.group_openid:
return Target(
event.group_openid,
source=str(event.id),
source=event.event_id or "",
adapter=self.get_adapter(),
self_id=bot.self_id if bot else None,
scope=SupportScope.qq_api,
Expand All @@ -205,19 +205,34 @@ def get_target(self, event: Event, bot: Bot | None = None) -> Target:
event.channel_id,
event.guild_id or "",
channel=True,
source=str(event.id),
source=event.event_id or "",
adapter=self.get_adapter(),
self_id=bot.self_id if bot else None,
scope=SupportScope.qq_api,
extra={"qq.interaction": True},
)
return Target(
event.get_user_id(),
private=True,
source=str(event.id),
adapter=self.get_adapter(),
self_id=bot.self_id if bot else None,
scope=SupportScope.qq_api,
)
if event.user_openid:
return Target(
event.user_openid,
private=True,
source=event.event_id or "",
adapter=self.get_adapter(),
self_id=bot.self_id if bot else None,
scope=SupportScope.qq_api,
extra={"qq.interaction": True},
)
if event.guild_id:
return Target(
event.guild_id,
channel=True,
private=True,
source=event.event_id or "",
adapter=self.get_adapter(),
self_id=bot.self_id if bot else None,
scope=SupportScope.qq_api,
extra={"qq.interaction": True},
)
raise NotImplementedError("QQ interaction has no reply destination")
if isinstance(event, FriendRobotEvent):
return Target(
event.openid,
Expand All @@ -236,9 +251,13 @@ def get_target(self, event: Event, bot: Bot | None = None) -> Target:
raise NotImplementedError

def get_message_id(self, event: Event) -> str:
if isinstance(event, InteractionCreateEvent):
if event.data.resolved.message_id is None:
raise NotImplementedError("QQ interaction has no source message ID")
return event.data.resolved.message_id
assert isinstance(
event,
(InteractionCreateEvent, GuildMessageEvent, C2CMessageCreateEvent, GroupMessageCreateEvent),
(GuildMessageEvent, C2CMessageCreateEvent, GroupMessageCreateEvent),
)
return str(event.id)

Expand Down Expand Up @@ -414,6 +433,24 @@ async def send_to(self, target: Target | Event, bot: Bot, message: Message, **kw
message = message.exclude("mention_channel")
return await bot.send(event=target, message=message, **kwargs)

if target.extra.get("qq.interaction", False):
if target.channel:
if target.private:
return await bot.send_to_dms(
guild_id=target.id, message=message, event_id=target.source or None, **kwargs
)
return await bot.send_to_channel(
channel_id=target.id, message=message, event_id=target.source or None, **kwargs
)
message = message.exclude("mention_channel")
if target.private:
return await bot.send_to_c2c(
openid=target.id, message=message, event_id=target.source or None, **kwargs
)
return await bot.send_to_group(
group_openid=target.id, message=message, event_id=target.source or None, **kwargs
)

if target.extra.get("qq.reply_seq") is not None:
target.extra["qq.reply_seq"] += 1

Expand Down Expand Up @@ -441,8 +478,6 @@ async def send_to(self, target: Target | Event, bot: Bot, message: Message, **kw
msg_ref_id=self.id_ref_cache.get(target.source),
**kwargs,
)
elif target.extra.get("qq.interaction", False):
return await bot.send_to_group(group_openid=target.id, message=message, event_id=target.source, **kwargs)
else:
res = await bot.send_to_group(
group_openid=target.id,
Expand Down
16 changes: 16 additions & 0 deletions src/nonebot_plugin_alconna/uniseg/adapters/qq/interaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from nonebot.adapters import Bot, Event
from nonebot.adapters.qq.bot import Bot as QQBot
from nonebot.adapters.qq.event import InteractionCreateEvent

from nonebot_plugin_alconna.uniseg.interaction import ButtonInteraction, ButtonInteractionAdapter


class QQButtonInteractionAdapter(ButtonInteractionAdapter):
async def extract_button(self, bot: Bot, event: Event) -> ButtonInteraction | None:
if not isinstance(bot, QQBot) or not isinstance(event, InteractionCreateEvent) or event.type != 11:
return None

await bot.put_interaction(interaction_id=event.id, code=0)

resolved = event.data.resolved
return ButtonInteraction(bot, event, resolved.button_id, resolved.button_data)
1 change: 1 addition & 0 deletions src/nonebot_plugin_alconna/uniseg/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ class SupportAdapterModule(str, Enum):
UNISEG_MESSAGE: Literal["_alc_uniseg_message"] = "_alc_uniseg_message"
UNISEG_TARGET: Literal["_alc_uniseg_target"] = "_alc_uniseg_target"
UNISEG_MESSAGE_ID: Literal["_alc_uniseg_message_id"] = "_alc_uniseg_message_id"
UNISEG_BUTTON_INTERACTION: Literal["_alc_uniseg_button_interaction"] = "_alc_uniseg_button_interaction"


class SerializeFailed(Exception): ...
120 changes: 120 additions & 0 deletions src/nonebot_plugin_alconna/uniseg/interaction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
from abc import ABC, abstractmethod
from datetime import datetime, timedelta
from typing import Any

from nonebot.adapters import Bot, Event
from nonebot.dependencies import Dependent
from nonebot.exception import SkippedException
from nonebot.matcher import Matcher
from nonebot.message import event_preprocessor
from nonebot.params import Depends
from nonebot.permission import Permission
from nonebot.plugin.on import get_matcher_source, store_matcher
from nonebot.rule import Rule
from nonebot.typing import T_Handler, T_PermissionChecker, T_RuleChecker, T_State

from .constraint import UNISEG_BUTTON_INTERACTION


class ButtonInteraction:
"""已完成必要平台回执的按钮事件。

``id`` 对应发送时的 ``Button.id``;平台未提供时为 ``None``。
``data`` 保留平台提供的回调文本,不从按钮标题或 ID 推测额外数据。
"""

def __init__(self, bot: Bot, event: Event, id: str | None, data: str | None = None):
self.bot = bot
self.event = event
self.id = id
self.data = data


class ButtonInteractionAdapter(ABC):
"""平台按钮回调适配:完成平台回执后返回统一事件。"""

@abstractmethod
async def extract_button(self, bot: Bot, event: Event) -> ButtonInteraction | None:
"""完成必要 ACK 并返回按钮上下文;其他事件返回 None。"""
raise NotImplementedError


async def get_button_interaction(bot: Bot, event: Event, state: T_State) -> ButtonInteraction | None:
"""取得本次事件分发的按钮上下文,不支持的平台或非按钮事件返回 None。

上下文保存在事件 state,普通 matcher 与嵌套 Waiter 共享同一次解析和 ACK。
"""
if UNISEG_BUTTON_INTERACTION in state:
return state[UNISEG_BUTTON_INTERACTION]
from .adapters import alter_get_interaction

adapter = alter_get_interaction(bot.adapter.get_name())
interaction = await adapter.extract_button(bot, event) if adapter is not None else None
state[UNISEG_BUTTON_INTERACTION] = interaction
return interaction


@event_preprocessor
async def _require_button_interaction(
interaction: ButtonInteraction | None = Depends(get_button_interaction),
) -> ButtonInteraction:
"""预处理与业务注入共用依赖缓存,避免并发重复 ACK。"""
if interaction is None:
raise SkippedException
return interaction


class _ButtonRule:
def __init__(self, ids: tuple[str, ...]):
self.ids = frozenset(ids)

async def __call__(
self,
interaction: ButtonInteraction | None = Depends(get_button_interaction),
) -> bool:
return interaction is not None and (not self.ids or interaction.id in self.ids)


def button_rule(*ids: str) -> Rule:
"""匹配原生按钮回调,不提供 ID 时匹配所有已支持的按钮事件。

规则只做匹配,不 ACK;可以组合其他 NoneBot 规则以及 Waiter。
"""
return Rule(_ButtonRule(ids))


def on_button(
*ids: str,
rule: Rule | T_RuleChecker | None = None,
permission: Permission | T_PermissionChecker | None = None,
handlers: list[T_Handler | Dependent[Any]] | None = None,
temp: bool = False,
expire_time: datetime | timedelta | None = None,
priority: int = 1,
block: bool = True,
state: T_State | None = None,
_depth: int = 0,
) -> type[Matcher]:
"""监听原生按钮点击,不经过命令解析,也不创建伪造消息事件。

handler、规则和 Waiter 统一通过 ``ButtonCallback`` 或
``UniversalButtonInteraction()`` 注入按钮上下文。

平台 ACK 已在事件预处理中完成,不受 ID、权限和业务规则匹配结果影响。
业务只判断是否执行操作,不管理平台回执。
其余参数与 NoneBot ``on()`` 相同;默认阻止继续传播。
"""
matcher = Matcher.new(
type_="",
rule=button_rule(*ids) & rule,
permission=Permission() | permission,
handlers=handlers,
temp=temp,
expire_time=expire_time,
priority=priority,
block=block,
default_state=state,
source=get_matcher_source(_depth + 1),
)
store_matcher(matcher)
return matcher
4 changes: 4 additions & 0 deletions src/nonebot_plugin_alconna/uniseg/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .builder import MessageBuilder
from .constraint import SupportAdapter
from .exporter import MessageExporter
from .interaction import ButtonInteractionAdapter
from .target import TargetFetcher


Expand All @@ -18,3 +19,6 @@ def get_exporter(self) -> MessageExporter: ...

def get_fetcher(self) -> TargetFetcher:
raise NotImplementedError

def get_interaction(self) -> ButtonInteractionAdapter:
raise NotImplementedError
7 changes: 7 additions & 0 deletions src/nonebot_plugin_alconna/uniseg/params.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from .constraint import UNISEG_MESSAGE, UNISEG_MESSAGE_ID, UNISEG_TARGET
from .exporter import SerializeFailed, Target
from .functions import get_message_id, get_target
from .interaction import ButtonInteraction, _require_button_interaction
from .message import UniMessage
from .segment import TS

Expand Down Expand Up @@ -78,7 +79,13 @@ async def _uni_seg(bot: Bot, event: Event, state: T_State) -> TS:
return Depends(_uni_seg, use_cache=True)


def UniversalButtonInteraction() -> ButtonInteraction:
"""在任意 matcher、规则或 Waiter 中注入已由平台适配层应答的按钮上下文。"""
return Depends(_require_button_interaction, use_cache=True)


UniMsg = Annotated[UniMessage, UniversalMessage()]
OriginalUniMsg = Annotated[UniMessage, UniversalMessage(origin=True)]
MsgId = Annotated[str, MessageId()]
MsgTarget = Annotated[Target, MessageTarget()]
ButtonCallback = Annotated[ButtonInteraction, UniversalButtonInteraction()]
Loading