Skip to content
This repository has been archived by the owner on Feb 20, 2024. It is now read-only.

Commit

Permalink
✨ 支持按钮消息 (#66)
Browse files Browse the repository at this point in the history
* ✨ support component message

* ✨ support ClickMsgComponentEvent

* ✨ support API /createComponentTemplate

* 🐛 fix union type error

Fuck Python 3.8
  • Loading branch information
MingxuanGame authored Dec 16, 2023
1 parent bed1979 commit 16fe28d
Show file tree
Hide file tree
Showing 5 changed files with 238 additions and 3 deletions.
31 changes: 31 additions & 0 deletions hertavilla/apis/message.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import json

from hertavilla.apis.internal import _BaseAPIMixin
from hertavilla.message.component import Panel
from hertavilla.message.internal import MsgContentInfo
from hertavilla.utils import MsgEncoder

Expand Down Expand Up @@ -97,3 +98,33 @@ async def recall_message(
"msg_time": msg_time,
},
)

async def create_component_template(
self,
villa_id: int,
panel: Panel,
) -> int:
"""创建消息组件模板,创建成功后会返回 template_id,
发送消息时,可以使用 template_id 填充 component_board
Args:
villa_id (int): 大别野 id
panel (Panel): 消息组件面板
Returns:
int: 组件模板id
"""
if template_id := (panel_dict := panel.to_dict()).get("template_id"):
return template_id
return int(
(
await self.base_request(
"/createComponentTemplate",
"POST",
villa_id,
data={
"panel": json.dumps(panel_dict),
},
)
)["template_id"],
)
27 changes: 26 additions & 1 deletion hertavilla/event.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ class AuditCallbackEvent(Event):
bot_tpl_id: str
"""机器人 id"""

room_id: int | None = None
room_id: Optional[int] = None
"""房间 id(和审核接口调用方传入的值一致)"""

user_id: int
Expand All @@ -271,6 +271,31 @@ def compare(self, audit_id: str, pass_through: str | None = None) -> bool:
return self.audit_id == audit_id and self.pass_through == pass_through


class ClickMsgComponentEvent(Event):
type: Literal[7]

room_id: int
"""房间 id"""

uid: int
"""用户 id"""

msg_uid: str
"""消息 id"""

bot_msg_id: str = ""
"""如果消息从属于机器人,则该字段不为空字符串"""

component_id: str
"""机器人自定义的组件id"""

template_id: int = 0
"""如果该组件模板为已创建模板,则template_id不为0"""

extra: str = ""
"""机器人自定义透传信息"""


def parse_event(payload: dict[str, Any]) -> Event:
type_: int = payload["type"]
cls_, name = events[type_]
Expand Down
11 changes: 9 additions & 2 deletions hertavilla/message/chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import sys
from typing import TYPE_CHECKING, Iterable, List

from hertavilla.message.component import Panel
from hertavilla.message.image import (
Image,
ImageMsgContentInfo,
Expand Down Expand Up @@ -71,19 +72,22 @@ def extend(self, obj: Iterable[_Segment]) -> Self:
self.append(segment)
return self

async def to_content_json(
async def to_content_json( # noqa: PLR0912
self,
bot: VillaBot,
) -> tuple[MsgContentInfo, str]:
text_entities = []
image = []
posts = []
panel: Panel | None = None

for segment in self:
if isinstance(segment, Image):
image.append(image_to_content(segment))
elif isinstance(segment, Post):
posts.append(post_to_content(segment))
elif isinstance(segment, Panel):
panel = segment
else:
text_entities.append(segment)

Expand Down Expand Up @@ -127,7 +131,10 @@ async def to_content_json(
"When post and text are present at the same time, "
"the post will not be displayed",
)
return await text_to_content(text_entities, bot, image), "MHY:Text"
return (
await text_to_content(text_entities, bot, image, panel),
"MHY:Text",
)

async def get_text(
self,
Expand Down
168 changes: 168 additions & 0 deletions hertavilla/message/component.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
from __future__ import annotations

from enum import IntEnum
from typing import TYPE_CHECKING, Any, List, Literal

from hertavilla.message.internal import _Segment

from pydantic import BaseModel, validator

if TYPE_CHECKING:
from hertavilla.bot import VillaBot


SMALL_MAX = 4
MID_MAX = 2
BIG_MAX = 1


class Component(BaseModel):
id: str # noqa: A003
"""组件id,由机器人自定义,不能为空字符串。面板内的id需要唯一"""

text: str
"""组件展示文本, 不能为空"""

type: int # noqa: A003
"""组件类型,目前支持 type=1 按钮组件,未来会扩展更多组件类型"""

need_callback: bool = True
"""是否订阅该组件的回调事件"""

extra: str = ""
"""组件回调透传信息,由机器人自定义"""


Comp = Component


class ComponentGroup(BaseModel):
__root__: List[Component]

def __init__(self, *components: Component):
super().__init__(__root__=components)


CGroup = ComponentGroup


class ComponentGroupList(BaseModel):
__root__: List[CGroup]

def __init__(self, *groups):
super().__init__(__root__=groups)


CGroupList = ComponentGroupList


class SGroup(CGroup):
def __init__(self, *components: Component):
super().__init__(*components)

@validator("__root__")
def check(cls, v: List[Component]) -> List[Component]:
if len(v) > SMALL_MAX:
raise ValueError(
f"small component group max length is {SMALL_MAX}",
)
return v


class MGroup(CGroup):
def __init__(self, *components: Component):
super().__init__(*components)

@validator("__root__")
def check(cls, v: List[Component]) -> List[Component]:
if len(v) > MID_MAX:
raise ValueError(f"mid component group max length is {MID_MAX}")
return v


class BGroup(CGroup):
def __init__(self, *components: Component):
super().__init__(*components)

@validator("__root__")
def check(cls, v: List[Component]) -> List[Component]:
if len(v) > BIG_MAX:
raise ValueError(f"big component group max length is {BIG_MAX}")
return v


LGroup = BGroup


class Panel(_Segment):
def __init__(
self,
template_id: int | None = None,
small: CGroupList | None = None,
mid: CGroupList | None = None,
big: CGroupList | None = None,
) -> None:
if not template_id and not (small or mid or big):
raise ValueError(
"At least one of template_id, component group must be set",
)
self.small = CGroupList() if small is None else small
self.mid = CGroupList() if mid is None else mid
self.big = CGroupList() if big is None else big

self.template_id = template_id

async def get_text(self, _: "VillaBot") -> str:
return "[Panel]"

def to_dict(
self,
) -> dict[str, Any]:
if (template_id := self.template_id) is not None:
return {"template_id": template_id}
return {
"small_component_group_list": self.small.dict()["__root__"],
"mid_component_group_list": self.mid.dict()["__root__"],
"big_component_group_list": self.big.dict()["__root__"],
}

# def insert(self, target: Literal["small", "mid", "big"], index: tuple[int, int], component: Component) -> None: # noqa: E501


class ButtonType(IntEnum):
"""组件交互类型"""

CALLBACK = 1
"""回传型"""
INPUT = 2
"""输入型"""
LINK = 3
"""跳转型"""


class Button(Component):
type: Literal[1] = 1 # noqa: A003

c_type: ButtonType
"""组件交互类型"""

input: str = "" # noqa: A003
"""如果交互类型为输入型,则需要在该字段填充输入内容,不能为空"""

link: str = ""
"""如果交互类型为跳转型,需要在该字段填充跳转链接,不能为空"""

need_token: bool = False
"""对于跳转链接来说,如果希望携带用户信息token,则need_token设置为true"""

@validator("input")
def check_input(cls, v, values):
if values["c_type"] == ButtonType.INPUT and not v:
raise ValueError("input is required")
return v

@validator("link")
def check_link(cls, v, values):
if values["c_type"] == ButtonType.LINK and not v:
raise ValueError("link is required")
return v
4 changes: 4 additions & 0 deletions hertavilla/message/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from typing import TYPE_CHECKING, Any, List, Literal, Optional, cast

from hertavilla.message.component import Panel
from hertavilla.message.image import ImageMsgContent
from hertavilla.message.internal import MsgContent, MsgContentInfo, _Segment
from hertavilla.typing import TypedDict
Expand All @@ -19,6 +20,7 @@ class TextMsgContentInfo(MsgContentInfo):
content: TextMsgContent
mentionedInfo: Optional[MentionedInfo]
quote: Optional[QuoteInfo]
panel: Optional[dict]


class QuoteInfo(TypedDict):
Expand Down Expand Up @@ -177,6 +179,7 @@ async def text_to_content(
text_entities: list[_TextEntity],
bot: VillaBot,
image: list[ImageMsgContent] | None = None,
panel: Panel | None = None,
) -> TextMsgContentInfo:
texts: list[str] = []
entities: list[EntityDict] = []
Expand Down Expand Up @@ -247,4 +250,5 @@ async def text_to_content(
),
"quote": quote,
"mentionedInfo": mentioned_info,
"panel": panel.to_dict() if panel is not None else None,
}

0 comments on commit 16fe28d

Please sign in to comment.