Skip to content

Commit

Permalink
✨ impl Button & Keyboard, for adapter discord, telegram and minecraft
Browse files Browse the repository at this point in the history
  • Loading branch information
RF-Tar-Railt committed Jul 17, 2024
1 parent 3753244 commit 40809de
Show file tree
Hide file tree
Showing 11 changed files with 261 additions and 39 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ async def _():
| 回复 Reply ||||| 🚫 || 🚫 | 🚫 |||| 🚫 | 🚫 |||||||
| 引用转发 Reference || 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | 🚫 || 🚫 | 🚫 | 🚫 | 🚫 ||| 🚫 || 🚫 |
| 超级消息 Hyper || 🚫 | 🚫 | ⬇️ | 🚫 || 🚫 | 🚫 |||| 🚫 | 🚫 | 🚫 || 🚫 | 🚫 || 🚫 |
| 按钮 Button | 🚫 | 🚫 | ⬆️ | 🚫 | 🚫 || 🚫 | 🚫 | 🚫 | 🚫 | 🚫 | ⬆️ | 🚫 || 🚫 || 🚫 || 🚫 |
| 其余 Other ||||||||||||||||||||


Expand Down
40 changes: 39 additions & 1 deletion src/nonebot_plugin_alconna/uniseg/adapters/discord/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

from nonebot.adapters import Bot, Event
from nonebot.adapters.discord import MessageEvent
from nonebot.adapters.discord.api.model import TextInput
from nonebot.adapters.discord.api.model import Button as ButtonModel
from nonebot.adapters.discord.message import (
StickerSegment,
ComponentSegment,
ReferenceSegment,
AttachmentSegment,
CustomEmojiSegment,
Expand All @@ -15,7 +18,7 @@

from nonebot_plugin_alconna.uniseg.constraint import SupportAdapter
from nonebot_plugin_alconna.uniseg.builder import MessageBuilder, build
from nonebot_plugin_alconna.uniseg.segment import At, AtAll, Emoji, Image, Reply
from nonebot_plugin_alconna.uniseg.segment import At, AtAll, Emoji, Image, Other, Reply, Button, Keyboard


class DiscordMessageBuilder(MessageBuilder):
Expand Down Expand Up @@ -55,6 +58,41 @@ def attachment(self, seg: AttachmentSegment):
def reference(self, seg: ReferenceSegment):
return Reply(seg.data["reference"].message_id, origin=seg.data["reference"]) # type: ignore

@build("component")
def component(self, seg: ComponentSegment):
comp = seg.data["component"]
if isinstance(comp, TextInput):
return Button(
flag="input",
id=comp.custom_id,
label=comp.label,
clicked_label=comp.placeholder or None, # type: ignore
)
buttons = []
for _comp in comp.components:
if isinstance(_comp, ButtonModel):
buttons.append(
Button(
flag="link" if isinstance(_comp.url, str) else "action",
id=_comp.custom_id or None, # type: ignore
label=_comp.label or "button", # type: ignore
url=_comp.url if isinstance(_comp.url, str) else None,
style=_comp.style.name.lower(),
)
)
elif isinstance(_comp, TextInput):
buttons.append(
Button(
flag="input",
id=_comp.custom_id,
label=_comp.label,
clicked_label=_comp.placeholder or None, # type: ignore
)
)
if buttons:
return Keyboard(buttons=buttons)
return Other(seg)

async def extract_reply(self, event: Event, bot: Bot):
if TYPE_CHECKING:
assert isinstance(event, MessageEvent)
Expand Down
53 changes: 50 additions & 3 deletions src/nonebot_plugin_alconna/uniseg/adapters/discord/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,29 @@
from tarina import lang
from nonebot.adapters import Bot, Event
from nonebot.internal.driver import Request
from nonebot.adapters.discord.api.types import ChannelType
from nonebot.adapters.discord.bot import Bot as DiscordBot
from nonebot.adapters.discord.api.model import Channel, MessageGet
from nonebot.adapters.discord.api.model import Button as ButtonModel
from nonebot.adapters.discord.message import Message, MessageSegment, parse_message
from nonebot.adapters.discord.api.types import ButtonStyle, ChannelType, TextInputStyle
from nonebot.adapters.discord.api.model import Channel, ActionRow, TextInput, MessageGet
from nonebot.adapters.discord.event import MessageEvent, GuildMessageCreateEvent, DirectMessageCreateEvent

from nonebot_plugin_alconna.uniseg.constraint import SupportScope
from nonebot_plugin_alconna.uniseg.segment import At, File, Text, AtAll, Audio, Emoji, Image, Reply, Video, Voice
from nonebot_plugin_alconna.uniseg.exporter import Target, SupportAdapter, MessageExporter, SerializeFailed, export
from nonebot_plugin_alconna.uniseg.segment import (
At,
File,
Text,
AtAll,
Audio,
Emoji,
Image,
Reply,
Video,
Voice,
Button,
Keyboard,
)


class DiscordMessageExporter(MessageExporter[Message]):
Expand Down Expand Up @@ -109,6 +123,39 @@ async def media(self, seg: Union[Image, Voice, Video, Audio, File], bot: Union[B
async def reply(self, seg: Reply, bot: Union[Bot, None]) -> "MessageSegment":
return MessageSegment.reference(seg.origin or int(seg.id), fail_if_not_exists=False)

def _button(self, seg: Button, bot: Union[Bot, None]):
styles = {
"primary": ButtonStyle.Primary,
"secondary": ButtonStyle.Secondary,
"success": ButtonStyle.Success,
"danger": ButtonStyle.Danger,
"link": ButtonStyle.Link,
}
label = str(seg.label)
if seg.flag == "link" and seg.url:
return ButtonModel(style=styles.get(seg.style or "", ButtonStyle.Primary), label=label, url=seg.url)
if seg.flag == "action" and seg.id:
return ButtonModel(style=styles.get(seg.style or "", ButtonStyle.Primary), label=label, custom_id=seg.id)
if seg.text:
return TextInput(
custom_id=seg.id or label,
style=TextInputStyle.Short,
label=label,
placeholder=seg.clicked_label or label,
value=seg.text,
)
raise SerializeFailed(lang.require("nbp-uniseg", "invalid_segment").format(type="button", seg=seg))

@export
async def button(self, seg: Button, bot: Union[Bot, None]) -> "MessageSegment":
return MessageSegment.component(self._button(seg, bot))

@export
async def keyboard(self, seg: Keyboard, bot: Union[Bot, None]) -> "MessageSegment":
if not seg.children:
raise SerializeFailed(lang.require("nbp-uniseg", "invalid_segment").format(type="keyboard", seg=seg))
return MessageSegment.component(ActionRow(components=[self._button(but, bot) for but in seg.children]))

async def send_to(self, target: Union[Target, Event], bot: Bot, message: Message):
assert isinstance(bot, DiscordBot)
if TYPE_CHECKING:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ def keyboard(self, seg: KeyboardSegment):
permission=perm,
)
)
return Keyboard(id=str(seg.data["bot_appid"]), buttons=buttons)
return Keyboard(id=str(seg.data["bot_appid"]), buttons=buttons, row=len(buttons) // len(seg.data["rows"]))

async def extract_reply(self, event: Event, bot: Bot):
if TYPE_CHECKING:
Expand Down
28 changes: 20 additions & 8 deletions src/nonebot_plugin_alconna/uniseg/adapters/kritor/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,17 +197,17 @@ def _button(self, seg: Button, bot: Union[Bot, None]):
perm = ButtonActionPermission(type=3, role_ids=[i.target for i in seg.permission])
else:
perm = ButtonActionPermission(type=0, user_ids=[i.target for i in seg.permission])

label = str(seg.label)
return ButtonModel(
id=seg.id or token_urlsafe(4),
render_data=ButtonRender(
label=seg.label,
visited_label=seg.clicked_label or seg.label,
label=label,
visited_label=seg.clicked_label or label,
style=0 if seg.style == "secondary" else 1,
),
action=ButtonAction(
type=0 if seg.flag == "link" else 1 if seg.flag == "action" else 2,
data=seg.url or seg.text or seg.label,
data=seg.url or seg.text or label,
enter=seg.flag == "enter",
unsupported_tips="该版本暂不支持查看此消息,请升级至最新版本。",
permission=perm,
Expand All @@ -225,7 +225,11 @@ async def keyboard(self, seg: Keyboard, bot: Union[Bot, None]):
if len(seg.children) > 25:
raise SerializeFailed(lang.require("nbp-uniseg", "invalid_segment").format(type="keyboard", seg=seg))
buttons = [self._button(child, bot) for child in seg.children]
return MessageSegment.keyboard(int(seg.id), [buttons[i : i + 5] for i in range(0, len(buttons), 5)])
if len(buttons) < 6 and not seg.row:
return MessageSegment("$kritor:button_row", {"buttons": buttons})
return MessageSegment.keyboard(
int(seg.id), [buttons[i : i + (seg.row or 5)] for i in range(0, len(buttons), seg.row or 5)]
)

async def send_to(self, target: Union[Target, Event], bot: Bot, message: Message):
assert isinstance(bot, KritorBot)
Expand All @@ -246,12 +250,20 @@ async def send_to(self, target: Union[Target, Event], bot: Bot, message: Message
for node in seg.data["nodes"]:
node.message.contact = contact.dump()
return await bot.send_forward_message(contact, seg.data["nodes"])
kb = None
if message.has("$kritor:button"):
buttons = [seg.data["button"] for seg in message.get("$kritor:button")]
message = message.exclude("$kritor:button")
message.append(
MessageSegment.keyboard(int(bot.self_id), [buttons[i : i + 5] for i in range(0, len(buttons), 5)])
)
kb = MessageSegment.keyboard(int(bot.self_id), [buttons[i : i + 5] for i in range(0, len(buttons), 5)])
if message.has("$kritor:button_row"):
rows = [seg.data["buttons"] for seg in message.get("$kritor:button_row")]
message = message.exclude("$kritor:button_row")
if not kb:
kb = MessageSegment.keyboard(int(bot.self_id), buttons=rows)
else:
kb.data["rows"] += rows
if kb:
message.append(kb)
if target.private:
return await bot.send_message(
contact=Contact(scene=SceneType.FRIEND, peer=target.id, sub_peer=None), elements=message.to_elements()
Expand Down
49 changes: 46 additions & 3 deletions src/nonebot_plugin_alconna/uniseg/adapters/minecraft/exporter.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from typing import Union

from tarina import lang
from nonebot.adapters import Bot, Event
from nonebot.adapters.minecraft.bot import Bot as MinecraftBot
from nonebot.adapters.minecraft.event.base import MessageEvent
from nonebot.adapters.minecraft.model import TextColor, BaseComponent
from nonebot.adapters.minecraft.message import Message, MessageSegment
from nonebot.adapters.minecraft.model import TextColor, ClickEvent, HoverEvent, ClickAction, HoverAction, BaseComponent

from nonebot_plugin_alconna.uniseg.segment import Text
from nonebot_plugin_alconna.uniseg.constraint import SupportScope
from nonebot_plugin_alconna.uniseg.exporter import Target, SupportAdapter, MessageExporter, export
from nonebot_plugin_alconna.uniseg.segment import Text, Button, Keyboard
from nonebot_plugin_alconna.uniseg.exporter import Target, SupportAdapter, MessageExporter, SerializeFailed, export

STYLE_TYPE_MAP = {
"b": "bold",
Expand Down Expand Up @@ -75,6 +76,48 @@ async def text(self, seg: Text, bot: Union[Bot, None]) -> "MessageSegment":
return MessageSegment.title(BaseComponent(text=seg.text, **kwargs))
return MessageSegment.text(seg.text, **kwargs)

@export
async def button(self, seg: Button, bot: Union[Bot, None]):
label = Text(seg.label) if isinstance(seg.label, str) else seg.label
styles = [STYLE_TYPE_MAP[s] for s in label.styles[(0, len(label.text))] if s in STYLE_TYPE_MAP]
kwargs = {}
for style in styles:
if style == "bold":
kwargs["bold"] = True
elif style == "italic":
kwargs["italic"] = True
elif style == "underline":
kwargs["underlined"] = True
elif style == "strikethrough":
kwargs["strikethrough"] = True
elif style == "obfuscated":
kwargs["obfuscated"] = True
else:
kwargs["color"] = style
if seg.clicked_label:
kwargs["hover_event"] = HoverEvent(
action=HoverAction.SHOW_TEXT, base_component_list=[BaseComponent(text=seg.clicked_label)]
)
if seg.flag == "link":
return MessageSegment.text(
label.text, **kwargs, click_event=ClickEvent(action=ClickAction.OPEN_URL, value=seg.url)
)
if seg.flag == "input":
return MessageSegment.text(
label.text, **kwargs, click_event=ClickEvent(action=ClickAction.SUGGEST_COMMAND, value=seg.text)
)
if seg.flag == "enter":
return MessageSegment.text(
label.text, **kwargs, click_event=ClickEvent(action=ClickAction.RUN_COMMAND, value=seg.text)
)
raise SerializeFailed(lang.require("nbp-uniseg", "invalid_segment").format(type="button", seg=seg))

@export
async def keyboard(self, seg: Keyboard, bot: Union[Bot, None]):
if seg.children:
return [await self.button(child, bot) for child in seg.children]
return MessageSegment.text("")

async def send_to(self, target: Union[Target, Event], bot: Bot, message: Message):
assert isinstance(bot, MinecraftBot)
return await bot.send_msg(message=message) # type: ignore
2 changes: 1 addition & 1 deletion src/nonebot_plugin_alconna/uniseg/adapters/qq/builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def keyboard(self, seg: KeyboardSegment):
permission=perm,
)
)
return Keyboard(buttons=buttons)
return Keyboard(buttons=buttons, row=len(buttons) // len(model.content.rows))

async def extract_reply(self, event: Event, bot: Bot):
if TYPE_CHECKING:
Expand Down
36 changes: 30 additions & 6 deletions src/nonebot_plugin_alconna/uniseg/adapters/qq/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ def __str__(self) -> str:
return "<$qq.button>"


@dataclass
class ButtonRowSegment(MessageSegment):

@override
def __str__(self) -> str:
return "<$qq.button_row>"


class QQMessageExporter(MessageExporter[Message]):
@classmethod
def get_adapter(cls) -> SupportAdapter:
Expand Down Expand Up @@ -295,16 +303,17 @@ def _button(self, seg: Button, bot: Union[Bot, None]):
perm = Permission(type=3, specify_role_ids=[i.target for i in seg.permission])
else:
perm = Permission(type=0, specify_user_ids=[i.target for i in seg.permission])
label = str(seg.label)
return ButtonModel(
id=seg.id,
render_data=RenderData(
label=seg.label,
visited_label=seg.clicked_label or seg.label,
label=label,
visited_label=seg.clicked_label or label,
style=0 if seg.style == "secondary" else 1,
),
action=Action(
type=0 if seg.flag == "link" else 1 if seg.flag == "action" else 2,
data=seg.url or seg.text,
data=seg.url or seg.text or label,
enter=seg.flag == "enter",
unsupport_tips="该版本暂不支持查看此消息,请升级至最新版本。",
permission=perm,
Expand All @@ -324,23 +333,38 @@ async def keyboard(self, seg: Keyboard, bot: Union[Bot, None]):
if len(seg.children) > 25:
raise SerializeFailed(lang.require("nbp-uniseg", "invalid_segment").format(type="keyboard", seg=seg))
buttons = [self._button(child, bot) for child in seg.children]
if len(buttons) < 6 and not seg.row:
return ButtonRowSegment("$qq:button_row", {"buttons": buttons})
rows = []
for i in range(0, len(buttons), 5):
rows.append(InlineKeyboardRow(buttons=buttons[i : i + 5]))
for i in range(0, len(buttons), seg.row or 5):
rows.append(InlineKeyboardRow(buttons=buttons[i : i + (seg.row or 5)]))
return MessageSegment.keyboard(MessageKeyboard(content=InlineKeyboard(rows=rows)))

async def send_to(self, target: Union[Target, Event], bot: Bot, message: Message):
assert isinstance(bot, QQBot)
if TYPE_CHECKING:
assert isinstance(message, self.get_message_type())

kb = None
if message.has("$qq:button"):
buttons = [seg.data["button"] for seg in message.get("$qq:button")]
message = message.exclude("$qq:button")
rows = []
for i in range(0, len(buttons), 5):
rows.append(InlineKeyboardRow(buttons=buttons[i : i + 5]))
message.append(MessageSegment.keyboard(MessageKeyboard(content=InlineKeyboard(rows=rows))))
kb = MessageKeyboard(content=InlineKeyboard(rows=rows))

if message.has("$qq:button_row"):
rows = [InlineKeyboardRow(buttons=seg.data["buttons"]) for seg in message.get("$qq:button_row")]
message = message.exclude("$qq:button_row")
if not kb:
kb = MessageKeyboard(content=InlineKeyboard(rows=rows))
else:
assert kb.content
assert kb.content.rows
kb.content.rows += rows
if kb:
message.append(MessageSegment.keyboard(kb))

if isinstance(target, Event):
assert isinstance(target, MessageEvent)
Expand Down
7 changes: 4 additions & 3 deletions src/nonebot_plugin_alconna/uniseg/adapters/satori/exporter.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,12 +105,13 @@ async def at_all(self, seg: AtAll, bot: Union[Bot, None]) -> "MessageSegment":

@export
async def button(self, seg: Button, bot: Union[Bot, None]) -> "MessageSegment":
label = str(seg.label)
if seg.flag == "action" and seg.id:
return MessageSegment.action_button(seg.id, seg.label, seg.style)
return MessageSegment.action_button(seg.id, label, seg.style)
elif seg.flag == "link" and seg.url:
return MessageSegment.link_button(seg.url, seg.label, seg.style)
return MessageSegment.link_button(seg.url, label, seg.style)
elif seg.text:
return MessageSegment.input_button(seg.text, seg.label, seg.style)
return MessageSegment.input_button(seg.text, label, seg.style)
else:
raise SerializeFailed(lang.require("nbp-uniseg", "invalid_segment").format(type="button", seg=seg))

Expand Down
Loading

0 comments on commit 40809de

Please sign in to comment.