diff --git a/README.md b/README.md index dc243d2..9b9e2b5 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,10 @@ _✨ 米游社大别野Bot Python SDK ✨_ ## 特性 -- 基于`FastAPI`,异步、快速、高性能! +- 基于`FastAPI`和`Pydantic`,异步、快速、高性能! +- 完整的类型注解支持 - 便捷的消息构造和发送方法 -- 完整的消息段和API支持 +- 丰富的消息段和完整的API支持 - ~~想不出来了~~ ## 安装 @@ -25,7 +26,7 @@ _✨ 米游社大别野Bot Python SDK ✨_ ## 快速开始 -首先你需要一个米游社大别野的Bot,如果没有请先自行申请,拿到`bot_id`、`bot_secret` +首先你需要一个[米游社大别野](https://dby.miyoushe.com/chat)的Bot,如果没有请先到机器人开发者社区(别野ID: OpenVilla)申请,取得`bot_id`、`bot_secret` ```python from villa import Bot diff --git a/pyproject.toml b/pyproject.toml index c418fb2..6f7feed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,14 @@ [tool.poetry] name = "villa" -version = "0.1.0" +version = "0.1.1" description = "米游社大别野Bot Python SDK。MiHoYo Villa Bot Python SDK." authors = ["CMHopeSunshine <277073121@qq.com>"] license = "MIT" readme = "README.md" +homepage = "https://github.com/CMHopeSunshine/villa-py" +repository = "https://github.com/CMHopeSunshine/villa-py" +documentation = "https://github.com/CMHopeSunshine/villa-py" +keywords = ["mihoyo", "bot", "villa"] [tool.poetry.dependencies] python = "^3.8" diff --git a/villa/bot.py b/villa/bot.py index 6301299..428691d 100644 --- a/villa/bot.py +++ b/villa/bot.py @@ -411,12 +411,14 @@ async def delete_room(self, villa_id: int, room_id: int) -> None: async def get_room(self, villa_id: int, room_id: int) -> Room: return Room.parse_obj( - await self._request( - "GET", - "vila/api/bot/platform/getRoom", - villa_id, - json={"room_id": room_id}, - ) + ( + await self._request( + "GET", + "vila/api/bot/platform/getRoom", + villa_id, + json={"room_id": room_id}, + ) + )["room"] ) async def get_villa_group_room_list(self, villa_id: int) -> GroupRoom: @@ -640,36 +642,39 @@ async def _handle_event(self, event: Event): ) async def _parse_message_content(self, message: Message) -> MessageContentInfo: - if quote := message["quote", 1]: + if quote := message["quote", 0]: quote = QuoteInfo(**quote.dict()) message_text = "" message_offset = 0 entities: List[TextEntity] = [] images: List[Image] = [] mentioned = MentionedInfo(type=MentionType.PART) - for i, seg in enumerate(message.__root__): + for seg in message: try: - space = " " if i != len(message) - 1 else "" if isinstance(seg, TextSegment): message_text += seg.content message_offset += len(seg.content) elif isinstance(seg, MentionAllSegment): - message_text += "@全体成员{space}" + message_text += f"@{seg.show_text} " entities.append( TextEntity( - offset=message_offset, length=6, entity=MentionedAll() + offset=message_offset, + length=6, + entity=MentionedAll(show_text=seg.show_text), ) ) - message_offset += 6 + message_offset += len(f"@{seg.show_text} ") mentioned.type = MentionType.ALL elif isinstance(seg, MentionRobotSegment): bot_name = self.bot_info.template.name if self.bot_info else "Bot" - message_text += f"@{bot_name}{space}" + message_text += f"@{bot_name} " entities.append( TextEntity( offset=message_offset, length=len(f"@{bot_name}".encode("utf-16")) // 2, - entity=MentionedRobot(bot_id=self.bot_id), + entity=MentionedRobot( + bot_id=self.bot_id, bot_name=bot_name + ), ) ) message_offset += len(f"@{bot_name}") + 1 @@ -677,12 +682,15 @@ async def _parse_message_content(self, message: Message) -> MessageContentInfo: elif isinstance(seg, MentionUserSegment): # 需要调用API获取被@的用户的昵称 user = await self.get_member(villa_id=seg.villa_id, uid=seg.user_id) - message_text += f"@{user.basic.nickname}{space}" + message_text += f"@{user.basic.nickname} " entities.append( TextEntity( offset=message_offset, length=len(f"@{user.basic.nickname}".encode("utf-16")) // 2, - entity=MentionedUser(user_id=str(user.basic.uid)), + entity=MentionedUser( + user_id=str(user.basic.uid), + user_name=user.basic.nickname, + ), ) ) message_offset += len(f"@{user.basic.nickname}") + 1 @@ -692,7 +700,7 @@ async def _parse_message_content(self, message: Message) -> MessageContentInfo: room = await self.get_room( villa_id=seg.villa_id, room_id=seg.room_id ) - message_text += f"#{room.room_name}{space}" + message_text += f"#{room.room_name} " entities.append( TextEntity( offset=message_offset, @@ -700,18 +708,21 @@ async def _parse_message_content(self, message: Message) -> MessageContentInfo: entity=VillaRoomLink( villa_id=str(seg.villa_id), room_id=str(seg.room_id), + room_name=room.room_name, ), ) ) message_offset += len(f"#{room.room_name} ") elif isinstance(seg, LinkSegment): show_text = seg.show_text or seg.url - message_text += show_text + space + message_text += show_text entities.append( TextEntity( offset=message_offset, length=len(show_text.encode("utf-16")) // 2, - entity=Link(url=seg.url), + entity=Link( + url=seg.url, show_text=seg.show_text or seg.url + ), ) ) message_offset += len(show_text) + 1 diff --git a/villa/event.py b/villa/event.py index 4e23bd5..f73d6a1 100644 --- a/villa/event.py +++ b/villa/event.py @@ -1,8 +1,7 @@ -import json from enum import IntEnum -from typing import Any, Dict, Type, Union, Optional +from typing import Dict, Type, Union, Optional -from pydantic import Extra, BaseModel, validator +from pydantic import Extra, BaseModel, root_validator from .store import _bots from .models import MessageContentInfo @@ -81,18 +80,80 @@ class SendMessageEvent(Event): """大别野ID""" bot_id: str """机器人ID""" + message: Message + """事件消息""" + + @root_validator(pre=True) + def _(cls, data: dict): + msg = Message() + msg_content_info = data["content"] + if quote := msg_content_info.get("quote"): + msg.append( + MessageSegment.quote( + message_id=quote["quoted_message_id"], + message_send_time=quote["quoted_message_send_time"], + ) + ) - @validator("content", pre=True) - def _content_str_to_dict(cls, v: Any): - if isinstance(v, str): - return json.loads(v) - return v - - @property - def message(self) -> Message: - if not hasattr(self, "_message"): - setattr(self, "_message", Message._parse(self.content, self.villa_id)) - return getattr(self, "_message") + content = msg_content_info["content"] + text = content["text"] + entities = content["entities"] + if not entities: + return Message(MessageSegment.text(text)) + text = text.encode("utf-16") + last_offset: int = 0 + last_length: int = 0 + for entity in entities: + end_offset: int = last_offset + last_length + offset: int = entity["offset"] + length: int = entity["length"] + entity_detail = entity["entity"] + if offset != end_offset: + msg.append( + MessageSegment.text( + text[((end_offset + 1) * 2) : ((offset + 1) * 2)].decode( + "utf-16" + ) + ) + ) + entity_text = text[(offset + 1) * 2 : (offset + length + 1) * 2].decode( + "utf-16" + ) + if entity_detail["type"] == "mentioned_robot": + entity_detail["bot_name"] = entity_text.lstrip("@")[:-1] + msg.append( + MessageSegment.mention_robot( + entity_detail["bot_id"], entity_detail["bot_name"] + ) + ) + elif entity_detail["type"] == "mentioned_user": + entity_detail["user_name"] = entity_text.lstrip("@")[:-1] + msg.append( + MessageSegment.mention_user( + int(entity_detail["user_id"]), data["villa_id"] + ) + ) + elif entity_detail["type"] == "mention_all": + entity_detail["show_text"] = entity_text.lstrip("@")[:-1] + msg.append(MessageSegment.mention_all(entity_detail["show_text"])) + elif entity_detail["type"] == "villa_room_link": + entity_detail["room_name"] = entity_text.lstrip("#")[:-1] + msg.append( + MessageSegment.room_link( + int(entity_detail["villa_id"]), + int(entity_detail["room_id"]), + ) + ) + else: + entity_detail["show_text"] = entity_text + msg.append(MessageSegment.link(entity_detail["url"], entity_text)) + last_offset = offset + last_length = length + end_offset = last_offset + last_length + if last_text := text[(end_offset + 1) * 2 :].decode("utf-16"): + msg.append(MessageSegment.text(last_text)) + data["message"] = msg + return data async def send( self, diff --git a/villa/message.py b/villa/message.py index ce9b0d3..7373714 100644 --- a/villa/message.py +++ b/villa/message.py @@ -33,7 +33,7 @@ def text(text: str) -> "Text": return Text(content=text) @staticmethod - def mention_robot(bot_id: str, bot_name: Optional[str] = None) -> "MentionRobot": + def mention_robot(bot_id: str, bot_name: str) -> "MentionRobot": return MentionRobot(bot_id=bot_id, bot_name=bot_name) @staticmethod @@ -41,8 +41,8 @@ def mention_user(villa_id: int, user_id: int) -> "MentionUser": return MentionUser(villa_id=villa_id, user_id=user_id) @staticmethod - def mention_all() -> "MentionAll": - return MentionAll() + def mention_all(show_text: str = "全体成员") -> "MentionAll": + return MentionAll(show_text=show_text) @staticmethod def room_link(villa_id: int, room_id: int) -> "RoomLink": @@ -99,7 +99,7 @@ class MentionRobot(MessageSegment): type: Literal["mention_robot"] = "mention_robot" bot_id: str - bot_name: Optional[str] = None + bot_name: str class MentionUser(MessageSegment): @@ -114,6 +114,7 @@ class MentionAll(MessageSegment): """@全体成员消息段""" type: Literal["mention_all"] = "mention_all" + show_text: str = "全体成员" class RoomLink(MessageSegment): @@ -220,7 +221,7 @@ def mention_all(self) -> Self: self.__root__.append(MentionAll()) return self - def mention_robot(self, bot_id: str, bot_name: Optional[str] = None) -> Self: + def mention_robot(self, bot_id: str, bot_name: str) -> Self: """提及(@at)机器人消息 参数: @@ -331,41 +332,6 @@ def append(self, segment: Union[str, MessageSegment]): segment = Text(content=segment) self.__root__.append(segment) - @classmethod - def _parse(cls, content: MessageContentInfo, villa_id: int) -> Self: - msg = cls() - text = content.content.text - text_begin = 0 - for entity in content.content.entities: - if isinstance(entity.entity, MentionedRobotInfo): - msg.mention_robot(entity.entity.bot_id) - elif isinstance(entity.entity, MentionedUserInfo): - msg.mention_user(villa_id, int(entity.entity.user_id)) - elif isinstance(entity.entity, MentionedAllInfo): - msg.mention_all() - elif isinstance(entity.entity, VillaRoomLinkInfo): - msg.room_link(int(entity.entity.villa_id), int(entity.entity.room_id)) - elif isinstance(entity.entity, LinkInfo): - msg.link(entity.entity.url, entity.entity.url) - if text_sengment := text[text_begin : entity.offset]: - msg.text(text_sengment) - text = text[(entity.offset + entity.length) :] - if text: - msg.text(text) - if content.content.images: - for image in content.content.images: - msg.image( - image.url, - image.size.width if image.size else None, - image.size.height if image.size else None, - image.file_size, - ) - if content.quote: - msg.quote( - content.quote.quoted_message_id, content.quote.quoted_message_send_time - ) - return msg - def plain_text(self) -> str: """获取纯文本消息内容""" return "".join( @@ -625,9 +591,9 @@ def __getitem__( if arg2 is None: return Message([seg for seg in self.__root__ if seg.type == arg1]) elif isinstance(arg2, int): - if l := [seg for seg in self.__root__ if seg.type == arg1]: - return l[arg2] - else: + try: + return [seg for seg in self.__root__ if seg.type == arg1][arg2] + except IndexError: return None elif isinstance(arg2, slice): return Message([seg for seg in self.__root__ if seg.type == arg1][arg2]) diff --git a/villa/models.py b/villa/models.py index 64e921a..6216d8c 100644 --- a/villa/models.py +++ b/villa/models.py @@ -52,6 +52,14 @@ def _add_villa_id_to_extend_data(cls, values: dict): if values.get("type") == 2 and "SendMessage" in values.get( "extend_data", {} ).get("EventData", {}): + if isinstance( + values["extend_data"]["EventData"]["SendMessage"]["content"], str + ): + values["extend_data"]["EventData"]["SendMessage"][ + "content" + ] = json.loads( + values["extend_data"]["EventData"]["SendMessage"]["content"] + ) if ( "villa_id" in values.get("robot", {}) and "villa_id" not in values["extend_data"]["EventData"]["SendMessage"] @@ -132,26 +140,36 @@ class MentionedRobot(BaseModel): type: Literal["mentioned_robot"] = "mentioned_robot" bot_id: str + bot_name: str = Field(exclude=True) + class MentionedUser(BaseModel): type: Literal["mentioned_user"] = "mentioned_user" user_id: str + user_name: str = Field(exclude=True) + class MentionedAll(BaseModel): type: Literal["mention_all"] = "mention_all" + show_text: str = Field(exclude=True) + class VillaRoomLink(BaseModel): type: Literal["villa_room_link"] = "villa_room_link" villa_id: str room_id: str + room_name: str = Field(exclude=True) + class Link(BaseModel): type: Literal["link"] = "link" url: str + show_text: str = Field(exclude=True) + class TextEntity(BaseModel): offset: int