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

Commit

Permalink
🔀 merge dev to master (#67)
Browse files Browse the repository at this point in the history
0.9.0
  • Loading branch information
MingxuanGame authored Dec 16, 2023
2 parents 512c168 + 16fe28d commit fe68fd0
Show file tree
Hide file tree
Showing 35 changed files with 3,038 additions and 691 deletions.
5 changes: 3 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,13 @@ ci:

repos:
- repo: https://github.com/psf/black
rev: 23.10.1
rev: 23.11.0
hooks:
- id: black
exclude: ^hertavilla/ws/pb/

- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.1.4
rev: v0.1.6
hooks:
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"fastapi",
"herta",
"hertavilla",
"Protobuf",
"Pubkey",
"pypi",
"redoc",
Expand Down
40 changes: 39 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ FastAPI 后端支持:
pip install herta-villa-sdk[fastapi]
```

WebSocket 支持:

```shell
pip install herta-villa-sdk[ws]
```

## 快速开始

你需要拥有一个[大别野](https://dby.miyoushe.com/chat)机器人。可前往大别野[「机器人开发者社区」](https://dby.miyoushe.com/chat/463/20020)`OpenVilla`)申请。
Expand All @@ -48,8 +54,10 @@ ccc
bot = VillaBot(
"bot_id", # 这里填写 bot_id
"bot_secret", # 这里填写 secret
"/", # bot 回调 endpoint
PUB_KEY, # 开放平台提供的 pub_key
callback_endpoint="/", # bot 回调 endpoint
# use_websocket=True, # 使用 WebSocket
# test_villa_id=0, # 测试别野,上线后可填 0
)


Expand All @@ -70,6 +78,34 @@ run(bot) # 运行 bot

详见 [examples](./examples/) 文件夹。

## WebSocket 支持

需在开放平台的「回调方式」中选择 「websocket回调」。

在实例化 `VillaBot` 时传入参数:

- `use_websocket` (bool) 启用 WebSocket
- `test_villa_id` (int) 测试别野,上线后可填 0

启动时会自动登录并接收事件。

### 手动下线

调用 `VillaBot.logout` 方法。

### Loop 后端

在不需要 HTTP WebHook 时,Herta SDK 提供了 `LoopBackend`

此后端仅依靠异步 `loop` 运行。

同时增加了监视 WS 连接(所有连接断开[^1]时会自动关闭应用)

[^1]:机器人主动 Logout 或者被服务器踢下线

- `auto_shutdown` (bool) 启用自动关闭
- `watch_interval` (int) 监视间隔

## 支持的 API

- [x] 鉴权
Expand Down Expand Up @@ -106,6 +142,7 @@ run(bot) # 运行 bot
- [x] 图片转存 `/transferImage`
- [x] 获取图片上传参数 `/getUploadImageParams`
- [x] 审核 `/audit`
- [x] 获取 WebSocket 接入信息 `/getWebsocketInfo`

## 支持的事件

Expand All @@ -125,6 +162,7 @@ run(bot) # 运行 bot
## 相关项目

- [CMHopeSunshine/villa-py](https://github.com/CMHopeSunshine/villa-py) 米游社大别野 Bot Python SDK(非官方)
- [CMHopeSunshine/nonebot-adapter-villa](https://github.com/CMHopeSunshine/nonebot-adapter-villa) NoneBot2 米游社大别野 Bot 适配器

## 交流

Expand Down
6 changes: 2 additions & 4 deletions hertavilla/apis/audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ async def audit(
self,
villa_id: int,
audit_content: str,
room_id: int,
uid: int,
room_id: int | None = None,
pass_through: str | None = None,
content_type: AuditContentType = AuditContentType.TEXT,
) -> str:
Expand All @@ -20,16 +20,14 @@ async def audit(
Args:
villa_id (int): 大别野 id
audit_content (str): 待审核内容
room_id (int): 房间 id
uid (int): 用户 id
room_id (int | None, optional): 房间 id,选填. Defaults to None.
pass_through (str | None, optional): 透传信息,该字段会在审核结果回调时携带给开发者,选填. Defaults to None.
content_type (AuditContentType, optional): 审核内容的类型. Defaults to AuditContentType.TEXT.
Returns:
str: 审核事件 id
""" # noqa: E501
# FIXME: 文档所说 room_id 和 uid 为选填
# 但是不填会 -1,所以这里设置成了必填
return (
await self.base_request(
"/audit",
Expand Down
12 changes: 6 additions & 6 deletions hertavilla/apis/internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ def __init__(
):
self.bot_id = bot_id
self.secret = secret
self.secret_encrypted = hmac.new(
pub_key.encode(),
secret.encode(),
hashlib.sha256,
).hexdigest()
self.pub_key = pub_key

def _make_header(self, villa_id: int) -> dict[str, str]:
secret = hmac.new(
self.pub_key.encode(),
self.secret.encode(),
hashlib.sha256,
).hexdigest()
return {
"x-rpc-bot_id": self.bot_id,
"x-rpc-bot_secret": secret,
"x-rpc-bot_secret": self.secret_encrypted,
"x-rpc-bot_villa_id": str(villa_id),
}

Expand Down
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"],
)
17 changes: 17 additions & 0 deletions hertavilla/apis/websocket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from __future__ import annotations

from hertavilla.apis.internal import _BaseAPIMixin
from hertavilla.model import WebSocketInfo


class WebSocketAPIMixin(_BaseAPIMixin):
async def get_websocket_info(self, villa_id: int) -> WebSocketInfo:
"""获取 WebSocket 接入信息
Args:
villa_id (int): 大别野 id
Returns:
WebSocketInfo: WebSocket 接入信息
"""
return await self.base_request("/getWebsocketInfo", "GET", villa_id)
25 changes: 23 additions & 2 deletions hertavilla/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from hertavilla.apis.role import RoleAPIMixin
from hertavilla.apis.room import RoomAPIMixin
from hertavilla.apis.villa import VillaAPIMixin
from hertavilla.apis.websocket import WebSocketAPIMixin
from hertavilla.match import (
Endswith,
EndswithResult,
Expand All @@ -43,6 +44,7 @@
if TYPE_CHECKING:
from hertavilla.event import Command, Event, SendMessageEvent, Template
from hertavilla.message import MessageChain
from hertavilla.ws.connection import WSConnection


TE = TypeVar("TE", bound="Event")
Expand Down Expand Up @@ -108,14 +110,18 @@ class VillaBot(
RoleAPIMixin,
ImgAPIMixin,
AuditAPIMixin,
WebSocketAPIMixin,
):
def __init__(
self,
bot_id: str,
secret: str,
callback_endpoint: str,
pub_key: str,
bot_info: "Template" | None = None,
*,
callback_endpoint: str = "/",
bot_info: "Template | None" = None,
use_websocket: bool = False,
test_villa_id: int = 0,
) -> None:
from hertavilla.event import SendMessageEvent

Expand All @@ -129,6 +135,10 @@ def __init__(
self.message_handlers: list[MessageHandler] = []
self.register_handler(SendMessageEvent, self.message_handler)

self.use_websocket = use_websocket
self.test_villa_id = test_villa_id
self.ws: "WSConnection | None" = None

@property
def bot_info(self) -> "Template":
assert (
Expand Down Expand Up @@ -362,3 +372,14 @@ def wrapper(
return func

return wrapper

async def logout(self) -> None:
if self.use_websocket is None:
raise RuntimeError(
f"WebSocket of {self.bot_id} is disabled",
)
if self.ws is None:
raise RuntimeError(
f"WebSocket of {self.bot_id} is not connected",
)
await self.ws.logout()
31 changes: 29 additions & 2 deletions 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
room_id: Optional[int] = None
"""房间 id(和审核接口调用方传入的值一致)"""

user_id: int
Expand All @@ -271,10 +271,37 @@ 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_]
data = payload["extend_data"]["EventData"][name]
extend = payload["extend_data"]
# support ws
data = extend["EventData"][name] if "EventData" in extend else extend[name]
payload.pop("extend_data")
payload |= data
return cls_.parse_obj(payload)
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
Loading

0 comments on commit fe68fd0

Please sign in to comment.