Skip to content

Commit 468f584

Browse files
authored
✨ 添加 Vocechat 适配器支持 (#137)
1 parent 6d5e1ee commit 468f584

4 files changed

Lines changed: 210 additions & 0 deletions

File tree

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from nonebot_plugin_alconna.uniseg.constraint import SupportAdapter
2+
from nonebot_plugin_alconna.uniseg.loader import BaseLoader
3+
4+
5+
class Loader(BaseLoader):
6+
def get_adapter(self) -> SupportAdapter:
7+
return SupportAdapter.vocechat
8+
9+
def get_builder(self):
10+
from .builder import VocechatMessageBuilder
11+
12+
return VocechatMessageBuilder()
13+
14+
def get_exporter(self):
15+
from .exporter import VocechatMessageExporter
16+
17+
return VocechatMessageExporter()
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from nonebot.adapters import Bot, Event
2+
from nonebot.adapters.vocechat.event import MessageEvent
3+
from nonebot.adapters.vocechat.message import MessageSegment
4+
5+
from nonebot_plugin_alconna.uniseg.builder import MessageBuilder, build
6+
from nonebot_plugin_alconna.uniseg.constraint import SupportAdapter
7+
from nonebot_plugin_alconna.uniseg.segment import At, Audio, File, Image, Reply, Text, Video
8+
9+
10+
class VocechatMessageBuilder(MessageBuilder[MessageSegment]):
11+
@classmethod
12+
def get_adapter(cls) -> SupportAdapter:
13+
return SupportAdapter.vocechat
14+
15+
@build("text")
16+
def text(self, seg: MessageSegment):
17+
return Text(seg.data["text"])
18+
19+
@build("markdown")
20+
def markdown(self, seg: MessageSegment):
21+
content = seg.data["text"]
22+
return Text(content).mark(0, len(content), "markdown")
23+
24+
@build("mention")
25+
def mention(self, seg: MessageSegment):
26+
return At("user", str(seg.data["user_id"]))
27+
28+
@build("image")
29+
def image(self, seg: MessageSegment):
30+
file = seg.data["file"]
31+
properties = seg.data.get("properties") or {}
32+
return Image(
33+
id=file.file_id,
34+
name=file.filename or Image.__default_name__,
35+
width=properties.get("width"),
36+
height=properties.get("height"),
37+
)
38+
39+
@build("audio")
40+
def audio(self, seg: MessageSegment):
41+
file = seg.data["file"]
42+
return Audio(id=file.file_id, name=file.filename or Audio.__default_name__)
43+
44+
@build("video")
45+
def video(self, seg: MessageSegment):
46+
file = seg.data["file"]
47+
return Video(id=file.file_id, name=file.filename or Video.__default_name__)
48+
49+
@build("file")
50+
def file(self, seg: MessageSegment):
51+
file = seg.data["file"]
52+
return File(id=file.file_id, name=file.filename or File.__default_name__)
53+
54+
async def extract_reply(self, event: Event, bot: Bot):
55+
if isinstance(event, MessageEvent) and event.reply:
56+
return Reply(str(event.reply.mid), event.reply.message, event.reply)
57+
return None
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
from pathlib import Path
2+
from typing import Any, Sequence
3+
4+
from nonebot.adapters import Bot, Event
5+
from nonebot.adapters.vocechat.bot import Bot as VcBot
6+
from nonebot.adapters.vocechat.event import GroupMessageEvent, MessageEvent
7+
from nonebot.adapters.vocechat.message import Message, MessageSegment
8+
from tarina import lang
9+
10+
from nonebot_plugin_alconna.uniseg.constraint import SupportScope
11+
from nonebot_plugin_alconna.uniseg.exporter import MessageExporter, SerializeFailed, SupportAdapter, Target, export
12+
from nonebot_plugin_alconna.uniseg.segment import At, Audio, File, Image, Reply, Segment, Text, Video, Voice
13+
14+
15+
class VocechatMessageExporter(MessageExporter[Message]):
16+
@classmethod
17+
def get_adapter(cls) -> SupportAdapter:
18+
return SupportAdapter.vocechat
19+
20+
def get_message_type(self):
21+
return Message
22+
23+
def get_target(self, event: Event, bot: Bot | None = None) -> Target:
24+
assert isinstance(event, MessageEvent)
25+
if isinstance(event, GroupMessageEvent) or event.target.gid is not None:
26+
return Target(
27+
str(event.target.gid),
28+
adapter=self.get_adapter(),
29+
self_id=bot.self_id if bot else None,
30+
scope=SupportScope.vocechat,
31+
)
32+
return Target(
33+
str(event.target.uid or event.from_uid),
34+
private=True,
35+
adapter=self.get_adapter(),
36+
self_id=bot.self_id if bot else None,
37+
scope=SupportScope.vocechat,
38+
)
39+
40+
def get_message_id(self, event: Event) -> str:
41+
assert isinstance(event, MessageEvent)
42+
return str(event.message_id or event.mid)
43+
44+
@export
45+
async def text(self, seg: Text, bot: Bot | None) -> MessageSegment:
46+
if seg.extract_most_style() == "markdown":
47+
return MessageSegment.markdown(seg.text)
48+
return MessageSegment.text(seg.text)
49+
50+
@export
51+
async def at(self, seg: At, bot: Bot | None) -> MessageSegment:
52+
if seg.flag != "user":
53+
raise SerializeFailed(lang.require("nbp-uniseg", "invalid_segment").format(type="at", seg=seg))
54+
return MessageSegment.mention(int(seg.target))
55+
56+
def _media_properties(self, seg: Image | Voice | Video | Audio | File) -> dict[str, Any] | None:
57+
properties: dict[str, Any] = {}
58+
if isinstance(seg, Image):
59+
if seg.width is not None:
60+
properties["width"] = seg.width
61+
if seg.height is not None:
62+
properties["height"] = seg.height
63+
if isinstance(seg, (Audio, Voice, Video)) and seg.duration is not None:
64+
properties["duration"] = seg.duration
65+
return properties or None
66+
67+
async def _media(self, seg: Image | Voice | Video | Audio | File, name: str) -> MessageSegment:
68+
method = {
69+
"image": MessageSegment.image,
70+
"voice": MessageSegment.audio,
71+
"audio": MessageSegment.audio,
72+
"video": MessageSegment.video,
73+
"file": MessageSegment.file,
74+
}[name]
75+
filename = None if seg.name == seg.__default_name__ else seg.name
76+
properties = self._media_properties(seg)
77+
if seg.id:
78+
return method(file_id=seg.id, filename=filename, properties=properties)
79+
if seg.path:
80+
return method(file=Path(seg.path), filename=filename, properties=properties)
81+
if seg.raw:
82+
return method(file=seg.raw_bytes, filename=filename, properties=properties)
83+
raise SerializeFailed(lang.require("nbp-uniseg", "invalid_segment").format(type=name, seg=seg))
84+
85+
@export
86+
async def media(self, seg: Image | Voice | Video | Audio | File, bot: Bot | None) -> MessageSegment:
87+
return await self._media(seg, seg.__class__.__name__.lower())
88+
89+
def _pop_reply(self, message: Message) -> tuple[int | None, Message]:
90+
reply_id: int | None = None
91+
new_message = self.get_message_type()([])
92+
for seg in message:
93+
if seg.type == "$vocechat:reply":
94+
if reply_id is None:
95+
reply_id = int(seg.data["mid"])
96+
continue
97+
new_message.append(seg)
98+
return reply_id, new_message
99+
100+
@export
101+
async def reply(self, seg: Reply, bot: Bot | None) -> MessageSegment:
102+
return MessageSegment("$vocechat:reply", {"mid": int(seg.id)})
103+
104+
async def send_to(self, target: Target | Event, bot: Bot, message: Message, **kwargs):
105+
assert isinstance(bot, VcBot)
106+
reply_id, message = self._pop_reply(message)
107+
if isinstance(target, MessageEvent):
108+
if reply_id is not None:
109+
return await bot.send_message(message=message, reply=reply_id, **kwargs)
110+
return await bot.send(target, message, **kwargs)
111+
if isinstance(target, Event):
112+
raise NotImplementedError
113+
if reply_id is not None:
114+
return await bot.send_message(message=message, reply=reply_id, **kwargs)
115+
if target.private:
116+
return await bot.send_message(message=message, user_id=int(target.id), **kwargs)
117+
return await bot.send_message(message=message, group_id=int(target.id), **kwargs)
118+
119+
async def recall(self, mid: Any, bot: Bot, context: Target | Event):
120+
assert isinstance(bot, VcBot)
121+
if isinstance(mid, str):
122+
return await bot.delete(int(mid))
123+
return await bot.delete(int(mid))
124+
125+
async def edit(self, new: Sequence[Segment], mid: Any, bot: Bot, context: Target | Event):
126+
assert isinstance(bot, VcBot)
127+
new_msg = await self.export(new, bot, True)
128+
_, new_msg = self._pop_reply(new_msg)
129+
return await bot.edit(int(mid), new_msg)
130+
131+
def get_reply(self, mid: Any):
132+
return Reply(str(mid))

src/nonebot_plugin_alconna/uniseg/constraint.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class SupportAdapter(str, Enum):
3737
wxmp = "WXMP"
3838
efchat = "EFChat"
3939
yunhu = "YunHu"
40+
vocechat = "VoceChat"
4041

4142
nonebug = "fake"
4243

@@ -75,6 +76,8 @@ class SupportScope(str, Enum):
7576
"""Bilibili直播平台"""
7677
yunhu = "YunHu"
7778
"""云湖平台"""
79+
vocechat = "VoceChat"
80+
"""VoceChat平台"""
7881

7982
onebot12_other = "Onebot12"
8083
"""ob12 的其他平台"""
@@ -147,6 +150,7 @@ class SupportAdapterModule(str, Enum):
147150
tail_chat = "nonebot_adapter_tailchat"
148151
wxmp = "nonebot.adapters.wxmp"
149152
yunhu = "nonebot.adapters.yunhu"
153+
vocechat = "nonebot.adapters.vocechat"
150154

151155

152156
UNISEG_MESSAGE: Literal["_alc_uniseg_message"] = "_alc_uniseg_message"

0 commit comments

Comments
 (0)