Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

适配 DoDo #114

Merged
merged 6 commits into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -473,3 +473,4 @@ $RECYCLE.BIN/
.vscode
pdm.lock
bot.py
.env*
37 changes: 19 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,30 +85,31 @@ assert deserialized_target == target

### 支持的 adapter

| OneBot v11 | OneBot v12 | QQ Guild | Kaiheila | Telegram | Feishu | Red |
| :--------: | :--------: | :------: | :------: | :------: | :----: | :-: |
| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| OneBot v11 | OneBot v12 | QQ Guild | Kaiheila | Telegram | Feishu | Red | DoDo |
| :--------: | :--------: | :------: | :------: | :------: | :----: | :-: | :--: |
| ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |

### 支持的消息类型

| | OneBot v11 | OneBot v12 | QQ Guild | 开黑啦 | Telegram | Feishu | Red |
| :--: | :--------: | :--------: | :------: | :----: | :------: | :----: | :-: |
| 文字 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 图片 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| at | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 回复 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 |
| | OneBot v11 | OneBot v12 | QQ Guild | 开黑啦 | Telegram | Feishu | Red | DoDo |
| :--: | :--------: | :--------: | :------: | :----: | :------: | :----: | :-: | :--: |
| 文字 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 图片 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| at | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| 回复 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | 🚧 | ✅ |

### 支持的发送目标

| | OneBot v11 | OneBot v12 | QQ Guild | Kaiheila | Telegram | Feishu | Red |
| :--------------------: | :--------: | :--------: | :------: | :------: | :------: | :----: | :-: |
| QQ 群 | ✅ | ✅ | | | | | ✅ |
| QQ 私聊 | ✅ | ✅ | | | | | ✅ |
| QQ 频道子频道消息 | | ✅ | ✅ | | | | |
| QQ 频道私聊 | | ✅ | ✅ | | | | |
| 开黑啦私聊/频道 | | | | ✅ | | | |
| Telegram 普通对话/频道 | | | | | ✅ | | |
| 飞书私聊/群聊 | | | | | | ✅ | |
| | OneBot v11 | OneBot v12 | QQ Guild | Kaiheila | Telegram | Feishu | Red | DoDo |
| :--------------------: | :--------: | :--------: | :------: | :------: | :------: | :----: | :-: | :--: |
| QQ 群 | ✅ | ✅ | | | | | ✅ | |
| QQ 私聊 | ✅ | ✅ | | | | | ✅ | |
| QQ 频道子频道消息 | | ✅ | ✅ | | | | | |
| QQ 频道私聊 | | ✅ | ✅ | | | | | |
| 开黑啦私聊/频道 | | | | ✅ | | | | |
| Telegram 普通对话/频道 | | | | | ✅ | | | |
| 飞书私聊/群聊 | | | | | | ✅ | | |
| DoDo 私聊/群聊 | | | | | | | | ✅ |
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

私聊的 TODO 是啥

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

啥?

@register_list_targets(adapter)
async def list_targets(bot: BaseBot) -> List[PlatformTarget]:
assert isinstance(bot, BotDodo)
targets = []
for island in await bot.get_island_list():
for channel in await bot.get_channel_list(
island_source_id=island.island_source_id
):
targets.append(TargetDoDoChannel(channel_id=channel.channel_id))
# TODO: 私聊
return targets

这个是 auto_select_bot 的


注:对于使用 Onebot v12,但是没有专门适配的发送目标,使用了 TargetOB12Unknow 来保证其可以正常使用

Expand Down
2 changes: 2 additions & 0 deletions nonebot_plugin_saa/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from .utils import SupportedAdapters as SupportedAdapters
from .registries import TargetQQPrivate as TargetQQPrivate
from .registries import TargetOB12Unknow as TargetOB12Unknow
from .registries import TargetDoDoChannel as TargetDoDoChannel
from .registries import TargetDoDoPrivate as TargetDoDoPrivate
from .registries import TargetFeishuGroup as TargetFeishuGroup
from .abstract_factories import MessageFactory as MessageFactory
from .registries import TargetFeishuPrivate as TargetFeishuPrivate
Expand Down
1 change: 1 addition & 0 deletions nonebot_plugin_saa/adapters/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from . import red as red
from . import dodo as dodo
from . import feishu as feishu
from . import qqguild as qqguild
from . import kaiheila as kaiheila
Expand Down
241 changes: 241 additions & 0 deletions nonebot_plugin_saa/adapters/dodo.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
from functools import partial
from contextlib import suppress
from typing import Any, Dict, List, Literal, Optional, cast

from nonebot import logger
from nonebot.adapters import Event
from nonebot.drivers import Request
from nonebot.adapters import Bot as BaseBot

from ..types import Text, Image, Reply, Mention
from ..auto_select_bot import register_list_targets
from ..utils import SupportedAdapters, SupportedPlatform
from ..abstract_factories import (
MessageFactory,
MessageSegmentFactory,
register_ms_adapter,
assamble_message_factory,
)
from ..registries import (
Receipt,
MessageId,
PlatformTarget,
TargetDoDoChannel,
TargetDoDoPrivate,
register_sender,
register_convert_to_arg,
register_target_extractor,
register_message_id_getter,
)

with suppress(ImportError):
from nonebot.adapters.dodo import Bot as BotDodo
from nonebot.adapters.dodo.models import MessageBody
from nonebot.adapters.dodo.message import Message, MessageSegment
from nonebot.adapters.dodo.event import (
MessageEvent,
GiftSendEvent,
ChannelArticleEvent,
ChannelMessageEvent,
MessageReactionEvent,
PersonalMessageEvent,
CardMessageFormSubmitEvent,
CardMessageListSubmitEvent,
ChannelArticleCommentEvent,
CardMessageButtonClickEvent,
ChannelVoiceMemberJoinEvent,
ChannelVoiceMemberLeaveEvent,
)

adapter = SupportedAdapters.dodo
register_dodo = partial(register_ms_adapter, adapter)

class DodoMessageId(MessageId):
adapter_name: Literal[adapter] = adapter

message_id: str
reason: Optional[None] = None

@register_message_id_getter(MessageEvent)
def _get_message_id(event: Event) -> DodoMessageId:
assert isinstance(event, MessageEvent)
return DodoMessageId(message_id=event.message_id)

@register_dodo(Text)
def _text(text: Text) -> MessageSegment:
return MessageSegment.text(text.data["text"])

@register_dodo(Image)
async def _image(image: Image, bot: BaseBot) -> MessageSegment:
if not isinstance(bot, BotDodo):
raise TypeError(f"Unsupported type of bot: {type(bot)}")

Check warning on line 71 in nonebot_plugin_saa/adapters/dodo.py

View check run for this annotation

Codecov / codecov/patch

nonebot_plugin_saa/adapters/dodo.py#L71

Added line #L71 was not covered by tests

file = image.data["image"]
if isinstance(file, str):
# 要求必须是官方链接,因此需要下载一遍
req = Request("GET", file, timeout=10)
resp = await bot.adapter.request(req)
if resp.status_code != 200:
raise RuntimeError(
f"Failed to download image: {resp.status_code}, url: {file}"
)
file = resp.content
if not isinstance(file, bytes):
raise TypeError(f"Unsupported type of file: {type(file)}, need bytes")

Check warning on line 84 in nonebot_plugin_saa/adapters/dodo.py

View check run for this annotation

Codecov / codecov/patch

nonebot_plugin_saa/adapters/dodo.py#L84

Added line #L84 was not covered by tests

upload_result = await bot.set_resouce_picture_upload(
file=file, file_name=image.data["name"] + ".png" # 上传是文件名必须携带有效后缀
)
logger.debug(f"Uploaded result: {upload_result}")
return MessageSegment.picture(**upload_result.dict())

@register_dodo(Reply)
def _reply(reply: Reply) -> MessageSegment:
assert isinstance(reply.data, DodoMessageId)
return MessageSegment.reference(reply.data.message_id)

@register_dodo(Mention)
def _mention(mention: Mention) -> MessageSegment:
return MessageSegment.at_user(dodo_id=mention.data["user_id"])

@register_target_extractor(ChannelMessageEvent)
def _extract_channel_msg_event(event: Event) -> TargetDoDoChannel:
assert isinstance(event, ChannelMessageEvent)
return TargetDoDoChannel(channel_id=event.channel_id)

@register_target_extractor(GiftSendEvent)
@register_target_extractor(ChannelArticleEvent)
@register_target_extractor(MessageReactionEvent)
@register_target_extractor(CardMessageFormSubmitEvent)
@register_target_extractor(CardMessageListSubmitEvent)
@register_target_extractor(ChannelArticleCommentEvent)
@register_target_extractor(CardMessageButtonClickEvent)
@register_target_extractor(ChannelVoiceMemberJoinEvent)
@register_target_extractor(ChannelVoiceMemberLeaveEvent)
def _extract_notice_event(event: Event) -> TargetDoDoChannel:
assert isinstance(
event,
(
GiftSendEvent,
ChannelArticleEvent,
MessageReactionEvent,
CardMessageFormSubmitEvent,
CardMessageListSubmitEvent,
ChannelArticleCommentEvent,
CardMessageButtonClickEvent,
ChannelVoiceMemberJoinEvent,
ChannelVoiceMemberLeaveEvent,
),
)
return TargetDoDoChannel(channel_id=event.channel_id)

@register_target_extractor(PersonalMessageEvent)
def _extract_personal_msg_event(event: Event) -> TargetDoDoPrivate:
assert isinstance(event, PersonalMessageEvent)
island_source_id = event.island_source_id
if island_source_id is None:
raise ValueError("island_source_id is None")
return TargetDoDoPrivate(
dodo_source_id=event.dodo_source_id, island_source_id=island_source_id
)

@register_convert_to_arg(adapter, SupportedPlatform.dodo_channel)
def _gen_channel(target: PlatformTarget) -> Dict[str, Any]:
assert isinstance(target, TargetDoDoChannel)
args = {
"channel_id": target.channel_id,
}
if target.dodo_source_id:
args["dodo_source_id"] = target.dodo_source_id
return args

@register_convert_to_arg(adapter, SupportedPlatform.dodo_private)
def _gen_private(target: PlatformTarget) -> Dict[str, Any]:
assert isinstance(target, TargetDoDoPrivate)
return {
"dodo_source_id": target.dodo_source_id,
"island_source_id": target.island_source_id,
}

class DodoReceipt(Receipt):
adapter_name: Literal[adapter] = adapter
message_id: str

async def revoke(self, reason: Optional[str] = None):
return await cast(BotDodo, self._get_bot()).set_channel_message_withdraw(
message_id=self.message_id, reason=reason
)

async def edit(self, mesaage_body: MessageBody):
return await cast(BotDodo, self._get_bot()).set_channel_message_edit(
message_id=self.message_id, message_body=mesaage_body
)

async def pin(self, is_cancel: bool = False):
"""置顶消息"""
return await cast(BotDodo, self._get_bot()).set_channel_message_top(
message_id=self.message_id, is_cancel=is_cancel
)

@property
def raw(self) -> str:
return self.message_id

@register_sender(adapter)
async def send(
bot,
msg: MessageFactory[MessageSegmentFactory],
target,
event,
at_sender: bool,
reply: bool,
) -> DodoReceipt:
assert isinstance(bot, BotDodo)
assert isinstance(target, (TargetDoDoChannel, TargetDoDoPrivate))

if event:
assert isinstance(event, MessageEvent)
full_msg = assamble_message_factory(
msg,
Mention(event.get_user_id()),
Reply(DodoMessageId(message_id=event.message_id)),
at_sender,
reply,
)
else:
full_msg = msg

message_to_send = Message()
for segment_factory in full_msg:
message_segment = await segment_factory.build(bot)
message_to_send += message_segment

if isinstance(target, TargetDoDoChannel):
if target.dodo_source_id:
resp = await bot.send_to_channel_personal(
message=message_to_send, **target.arg_dict(bot)
)
else:
resp = await bot.send_to_channel(
message=message_to_send, **target.arg_dict(bot)
)
else:
resp = await bot.send_to_personal(
message=message_to_send, **target.arg_dict(bot)
)

return DodoReceipt(message_id=resp, bot_id=bot.self_id)

@register_list_targets(adapter)
async def list_targets(bot: BaseBot) -> List[PlatformTarget]:
assert isinstance(bot, BotDodo)
targets = []
for island in await bot.get_island_list():
for channel in await bot.get_channel_list(
island_source_id=island.island_source_id
):
targets.append(TargetDoDoChannel(channel_id=channel.channel_id))

# TODO: 私聊

return targets
2 changes: 2 additions & 0 deletions nonebot_plugin_saa/registries/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
from .platform_send_target import register_sender as register_sender
from .platform_send_target import TargetOB12Unknow as TargetOB12Unknow
from .platform_send_target import QQGuildDMSManager as QQGuildDMSManager
from .platform_send_target import TargetDoDoChannel as TargetDoDoChannel
from .platform_send_target import TargetDoDoPrivate as TargetDoDoPrivate
from .platform_send_target import TargetFeishuGroup as TargetFeishuGroup
from .platform_send_target import TargetFeishuPrivate as TargetFeishuPrivate
from .platform_send_target import TargetQQGuildDirect as TargetQQGuildDirect
Expand Down
30 changes: 30 additions & 0 deletions nonebot_plugin_saa/registries/platform_send_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,36 @@ class TargetFeishuGroup(PlatformTarget):
chat_id: str


class TargetDoDoChannel(PlatformTarget):
"""DoDo Channel

参数
channel_id: 频道ID
dodo_source_id: 用户 ID(可选)
"""

platform_type: Literal[
SupportedPlatform.dodo_channel
] = SupportedPlatform.dodo_channel
channel_id: str
dodo_source_id: Optional[str] = None


class TargetDoDoPrivate(PlatformTarget):
"""DoDo Private

参数
dodo_source_id: 用户 ID
island_source_id: 群 ID
"""

platform_type: Literal[
SupportedPlatform.dodo_private
] = SupportedPlatform.dodo_private
island_source_id: str
dodo_source_id: str


# this union type is for deserialize pydantic model with nested PlatformTarget
AllSupportedPlatformTarget = Union[
TargetQQGroup,
Expand Down
3 changes: 3 additions & 0 deletions nonebot_plugin_saa/utils/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class SupportedAdapters(StrEnum):
telegram = "Telegram"
feishu = "Feishu"
red = "RedProtocol"
dodo = "DoDo"

fake = "fake" # for nonebug

Expand All @@ -25,6 +26,8 @@ class SupportedPlatform(StrEnum):
telegram_forum = "Telegram Forum"
feishu_private = "Feishu Private"
feishu_group = "Feishu Group"
dodo_channel = "DoDo Channel"
dodo_private = "DoDo Private"


supported_adapter_names = set(SupportedAdapters._member_map_.values()) # noqa: SLF001
Loading