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

RFC: 适配器规范 #2435

Open
RF-Tar-Railt opened this issue Oct 31, 2023 · 11 comments
Open

RFC: 适配器规范 #2435

RF-Tar-Railt opened this issue Oct 31, 2023 · 11 comments
Labels
enhancement New feature or request

Comments

@RF-Tar-Railt
Copy link
Member

RF-Tar-Railt commented Oct 31, 2023

希望能解决的问题

Nonebot现在的适配器很多,但是基本上都是各写各的,没有相对的统一结构,甚至存在代码质量的问题(e.g. Bilibili适配器)。

在这份issue中,我们提出了一个通用的适配器规范,其不会影响适配器的平台特性,并且能为用户提供更好的使用体验。

描述所需要的功能

以下规范依据文件名划分

adapter.py

  1. 对于网络请求,适配器不应当依赖特定的网络层框架/库(e.g. httpx)。
  2. 适配器的网络请求或 ws 连接应通过驱动器的 Request 进行。若存在所使用的驱动器无法满足平台条件的情况,[1] 条可忽略。
  3. 基于 [1][2],适配器应在 setup 阶段检查驱动器类型是否符合要求:
    def setup(self) -> None:
        if not isinstance(self.driver, HTTPClientMixin):
            raise RuntimeError(
                f"Current driver {self.config.driver} does not support http client requests! "
                f"{adapter} Adapter need a HTTPClient Driver to work."
            )
        if not isinstance(self.driver, WebSocketClientMixin):
            raise RuntimeError(
                f"Current driver {self.config.driver} does not support websocket client! "
                f"{adapter} Adapter need a WebSocketClient Driver to work."
            )
        self.driver.on_ready(self.prepare_connection)
        self.driver.on_shutdown(self.close_connection)
  4. 对于网络请求,若请求时出现异常,适配器应抛出 NetworkError;若请求的响应状态码异常(e.g. 404),适配器应根据情况抛出对应的异常。

exception.py

  1. 基于[4],适配器应继承 nonebot.exception 中的基础异常类型,声明适配器特定异常:
    import json
    from typing import Optional
    
    from nonebot.drivers import Response
    from nonebot.exception import AdapterException
    from nonebot.exception import ActionFailed as BaseActionFailed
    from nonebot.exception import NetworkError as BaseNetworkError
    from nonebot.exception import ApiNotAvailable as BaseApiNotAvailable
    
    
    class XXXAdapterException(AdapterException):
        def __init__(self):
            super().__init__("xxx")
    
    class NetworkError(BaseNetworkError, XXXAdapterException):
        def __init__(self, msg: Optional[str] = None):
            super().__init__()
            self.msg: Optional[str] = msg
            """错误原因"""
    
        def __repr__(self):
            return f"<NetWorkError message={self.msg}>"
    
        def __str__(self):
            return self.__repr__()
    
    class ActionFailed(BaseActionFailed, XXXAdapterException):
        def __init__(self, response: Response):
            self.status_code: int = response.status_code
            self.code: Optional[int] = ...
            self.message: Optional[str] = ...
            self.data: Optional[dict] = ...
    
    class UnauthorizedException(ActionFailed):
        pass
    
    class RateLimitException(ActionFailed):
        pass
    
    class ApiNotAvailable(BaseApiNotAvailable, XXXAdapterException):
        pass	

event.py

  1. Event 应存在 time 字段,表示事件创建的时间。time 为 datetime.datetime 类型。

  2. MessageEvent 应存在如下字段:

    • to_me: bool 类型,可选(因为主要是 is_tome 方法)。
    • reply: 一般为 reply 对应的原始消息/原始事件(由 reply 上的 msgid获取);同时也可以为自定义结构(e.g. ob12适配器下的Reply),但是应当挂载一个 async def get_origin() 方法,以获取原始事件;若平台不存在回复元素,置空即可。
    • message/_message: 适配器对应的 Message 类型。若原事件已存在 message字段并无法转换类型,则使用 _message
    • original_message: 适配器对应的 Message 类型,并且未经过 check_at_me, check_reply 等处理。
    • message_id: 消息id (有时与事件id等同),用于构造回复消息,撤回消息,编辑消息等操作;若平台不存在消息id,使用 "" 或随机生成即可。

    其中 _message, original_message 可如下处理:

    from copy import deepcopy
    from typing import TYPE_CHECKING
    from .message import Message
    
    class MessageEvent(Event):
    	if TYPE_CHECKING:
    		message: Message
    		original_message: Message
    	
    	def get_message(self):
    		if not hasattr(self, "message"):
    			msg = Message(xxx)
    			setattr(self, "message", msg)
    			setattr(self, "original_message", deepcopy(msg))
    		return getattr(self, "message")

bot.py

  1. 适配器应当在 handle_event内执行 check_reply, check_at_me, check_nickname。

    • check_reply: 检查消息序列或事件中是否存在 reply 数据,无论是否回复 bot 自己,有则移除 reply 消息段,并将 reply 对应的事件赋值给 reply 属性。在此之上,若回复的是 bot 自己的消息,则设 to_me 为真。
    • check_at_me:检查消息首部或尾部是否是 @bot_self, 如果是则移除,并且连带@后随的空格一并移除,设 to_me 为真。尾部 at 不强制要求。
    • check_nickname: 如果机器人配置了nickname,则检查消息首部是否为昵称,同 check_at_me。
  2. 适配器应当在 Bot 上编写常用的方法/接口,并写明每个接口的输入类型和输出类型。如果对接协议不存在或未允许扩展api,请将Bot的__getattr__方法明确写为不支持(def __getattr__(self, item): raise NotImplementError);否则需要编写 bot.pyi 或生成全部的可用方法,而不是完全让用户使用call_api。

  3. Bot 应声明自己的 adapter属性为适配器对应的 Adapter 类型:

    from typing import TYPE_CHECKING, override
    from nonebot.adapters import Bot as BaseBot
    
    if TYPE_CHECKING:
    	from .adapter import Adapter
    
    class Bot(BaseBot):
    	adapter: "Adapter"
    	
    	@override
    	def __init__(self, adapter: "Adapter", self_id: str,  **kwargs):
    		...

message.py

  1. 适配器应在 message.py 内编写原始数据转为消息序列的方法(e.g. Message.from_guild_message)。
  2. 消息段应尽量使用子类消息段+父类消息段静态方法合集:
    class MessageSegment(BaseMessageSegment):
    	@staticmethod
    	def text(text: str) -> "Text":
    		return Text("text", {"text": text})
    
    class Text(MessageSegment):
    	@override
    	def __str__(self):
    		return self.data["text"]
  3. MessageSegment.text 的 data 必须为 {"text": xxx}
  4. 对于某些平台存在元素包含特殊子元素的情况(例如,kook平台的 KMarkdown 包含 mention, emoji等子元素),适配器应特殊处理,将这些子元素提取为单独的消息段。

utils.py

  1. 适配器的日志部分应使用 logger_wrapper 返回的 log 进行:
    from nonebot.utils import logger_wrapper
    log = logger_wrapper("XXX Adapter")

README.md

  1. 适配器的 README 应当写明自己的配置项,至少需要说明自己需要的驱动器类型。

pyproject.toml

  1. 适配器的最低 python 依赖版本应跟随 nonebot2 要求的最低版本。
@RF-Tar-Railt RF-Tar-Railt added the enhancement New feature or request label Oct 31, 2023
@GreyElaina
Copy link

Q: 如果对接形似 python-satori 或者极端一点的 avilla,网络通信的部分应如何处理?

@RF-Tar-Railt
Copy link
Member Author

Q: 如果对接形似 python-satori 或者极端一点的 avilla,网络通信的部分应如何处理?

A: 适配器的网络通信部分针对的是适配器通过网络通信与对应平台交互的情况。若适配器使用的是另外的sdk,则适配器本体不关心网络部分。但是相应的,适配器应对sdk做类似处理(e.g. 挂载运行任务,处理异常返回)

@GreyElaina
Copy link

check_at_me:检查消息首部或尾部是否是 @bot_self, 如果是则移除,并且连带@后随的空格一并移除,设 to_me 为真。尾部 at 不强制要求。

Q: 存疑的,例如 Telegram 平台中,对于调用某个 Bot 的指令,可以用 /some_command@some_bot 的形式,这种似乎不符合 “尾部或首部” 的定义……?

@RF-Tar-Railt
Copy link
Member Author

check_at_me:检查消息首部或尾部是否是 @bot_self, 如果是则移除,并且连带@后随的空格一并移除,设 to_me 为真。尾部 at 不强制要求。

Q: 存疑的,例如 Telegram 平台中,对于调用某个 Bot 的指令,可以用 /some_command@some_bot 的形式,这种似乎不符合 “尾部或首部” 的定义……?

按ddl说法,这种情况的 @ 确实得移除(只要是 @ 当前bot自己的)

@CMHopeSunshine
Copy link
Member

可以在适配器编写教程中写一写吧

@RF-Tar-Railt
Copy link
Member Author

RF-Tar-Railt commented Nov 1, 2023

提案:对于存在 "回复消息" 的适配器,是否强制要求在消息段层面实现一个 MessageSegment.reply,或要求在 send 接口增加与 reply 相关的参数?
(例如,feishu 适配器有专门的接口发送回复消息,tg适配器有专门的参数指定回复元素,而 red,sattori,qq 适配器则可以直接在 Message 中构造回复元素)

@GreyElaina
Copy link

我的评价是: avilla.standard.core

@RF-Tar-Railt
Copy link
Member Author

新增:第 13条 MessageSegment.text 的 data 必须为 {"text": xxx}
更新:第 7条

@RF-Tar-Railt
Copy link
Member Author

RF-Tar-Railt commented Nov 9, 2023

@ssttkkl @Tian-que

@RF-Tar-Railt
Copy link
Member Author

新增:第14条 对于某些平台存在元素包含特殊子元素的情况(例如,kook平台的 KMarkdown 包含 mention, emoji等子元素),适配器应特殊处理,将这些子元素提取为单独的消息段。
@ssttkkl @Tian-que

@RF-Tar-Railt
Copy link
Member Author

RF-Tar-Railt commented Mar 22, 2024

更新第9条:
适配器应当在 Bot 上编写常用的方法/接口,并写明每个接口的输入类型和输出类型。如果对接协议不存在或未允许扩展api,请将Bot的__getattr__方法明确写为不支持(def __getattr__(self, item): raise NotImplementError);否则需要编写 bot.pyi 或生成全部的可用方法,而不是完全让用户使用call_api。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Development

No branches or pull requests

3 participants