nonebot2/website/versioned_docs/version-2.0.0-beta.2/tutorial/process-message.md
Mix 43aebd9c93
Docs: 修改消息模板文档中错误的样例 (#806)
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: Ju4tCode <42488585+yanyongyu@users.noreply.github.com>
2022-02-16 20:24:00 +08:00

8.1 KiB
Raw Blame History

sidebar_position description options
9 处理消息序列与消息段
menu
weight category
30 guide

处理消息

NoneBot2 中的消息

在不同平台中,一条消息可能会有承载有各种不同的表现形式,它可能是一段纯文本、一张图片、一段语音、一篇富文本文章,也有可能是多种类型的组合等等。

在 NoneBot2 中,为确保消息的正常处理与跨平台兼容性,采用了扁平化的消息序列形式,即 Message 对象。

Message 是多个消息段 MessageSegment 的集合,它继承自 List[MessageSegment],并在此基础上添加或强化了一些特性。

MessageSegment 是一个 dataclass ,它具有一个类型标识 type,以及一些对应的数据信息 data

此外NoneBot2 还提供了 MessageTemplate ,用于构建支持消息序列以及消息段的特殊消息模板。

使用消息序列

通常情况下,适配器在接收到消息时,会将消息转换为消息序列,可以通过 EventMessage 作为依赖注入, 或者使用 event.get_message() 获取。

由于它是List[MessageSegment]的子类, 所以你总是可以用和操作 List 类似的方式来处理消息序列

>>> message = Message([
    MessageSegment(type='text', data={'text':'hello'}),
    MessageSegment(type='image', data={'url':'http://example.com/image.png'}),
    MessageSegment(type='text', data={'text':'world'}),
])
>>> for segment in message:
...     print(segment.type, segment.data)
...
text {'text': 'hello'}
image {'url': 'http://example.com/image.png'}
text {'text': 'world'}
>>> len(message)
3

构造消息序列

在使用事件响应器操作发送消息时,既可以使用 str 作为消息,也可以使用 MessageMessageSegment 或者 MessageTemplate。那么,我们就需要先构造一个消息序列。

直接构造

Message 类可以直接实例化,支持 strMessageSegmentIterable[MessageSegment] 或适配器自定义类型的参数。

# str
Message("Hello, world!")
# MessageSegment
Message(MessageSegment.text("Hello, world!"))
# List[MessageSegment]
Message([MessageSegment.text("Hello, world!")])

运算构造

Message 对象可以通过 strMessageSegment 相加构造,详情请参考拼接消息

从字典数组构造

Message 对象支持 Pydantic 自定义类型构造,可以使用 Pydantic 的 parse_obj_as (parse_raw_as) 方法进行构造。

from pydantic import parse_obj_as

# 由字典构造消息段
parse_obj_as(
    MessageSegment, {"type": "text", "data": {"text": "text"}}
) == MessageSegment.text("text")
# 由字典数组构造消息序列
parse_obj_as(
    Message,
    [MessageSegment.text("text"), {"type": "text", "data": {"text": "text"}}],
) == Message([MessageSegment.text("text"), MessageSegment.text("text")])

:::tip 提示 以上示例中的字典数据仅做参考,具体的数据格式由适配器自行定义。 :::

获取消息纯文本

由于消息中存在各种类型的消息段,因此 str(message) 通常并不能得到消息的纯文本,而是一个消息序列的字符串表示。

NoneBot2 为消息段定义了一个方法 is_text() ,可以用于判断消息段是否为纯文本;也可以使用 message.extract_plain_text() 方法获取消息纯文本。

# 判断消息段是否为纯文本
MessageSegment.text("text").is_text() == True
# 提取消息纯文本字符串
Message(
    [MessageSegment.text("text"), MessageSegment.at(123)]
).extract_plain_text() == "text"

遍历

Message 继承自 List[MessageSegment] ,因此可以使用 for 循环遍历消息段。

for segment in message:
    ...

索引与切片

Message 对列表的索引与切片进行了增强,在原有列表 int 索引与切片的基础上,支持 type 过滤索引与切片。

message = Message(
    [
        MessageSegment.text("test"),
        MessageSegment.image("test2"),
        MessageSegment.image("test3"),
        MessageSegment.text("test4"),
    ]
)

# 索引
message[0] == MessageSegment.text("test")
# 切片
message[0:2] == Message(
    [MessageSegment.text("test"), MessageSegment.image("test2")]
)

# 类型过滤
message["image"] == Message(
    [MessageSegment.image("test2"), MessageSegment.image("test3")]
)
# 类型索引
message["image", 0] == MessageSegment.image("test2")
# 类型切片
message["image", 0:2] == Message(
    [MessageSegment.image("test2"), MessageSegment.image("test3")]
)

同样的,Message 对列表的 indexcount 方法也进行了增强,可以用于索引指定类型的消息段。

# 指定类型首个消息段索引
message.index("image") == 1
# 指定类型消息段数量
message.count("image") == 2

此外,Message 添加了一个 get 方法,可以用于获取指定类型指定个数的消息段。

# 获取指定类型指定个数的消息段
message.get("image", 1) == Message([MessageSegment.image("test2")])

拼接消息

strMessageMessageSegment 对象之间可以直接相加,相加均会返回一个新的 Message 对象。

# 消息序列与消息段相加
Message([MessageSegment.text("text")]) + MessageSegment.text("text")
# 消息序列与字符串相加
Message([MessageSegment.text("text")]) + "text"
# 消息序列与消息序列相加
Message([MessageSegment.text("text")]) + Message([MessageSegment.text("text")])
# 字符串与消息序列相加
"text" + Message([MessageSegment.text("text")])

# 消息段与消息段相加
MessageSegment.text("text") + MessageSegment.text("text")
# 消息段与字符串相加
MessageSegment.text("text") + "text"
# 消息段与消息序列相加
MessageSegment.text("text") + Message([MessageSegment.text("text")])
# 字符串与消息段相加
"text" + MessageSegment.text("text")

如果需要在当前消息序列后直接拼接新的消息段,可以使用 Message.appendMessage.extend 方法,或者使用自加。

msg = Message([MessageSegment.text("text")])
# 自加
msg += "text"
msg += MessageSegment.text("text")
msg += Message([MessageSegment.text("text")])
# 附加
msg.append("text")
msg.append(MessageSegment.text("text"))
# 扩展
msg.extend([MessageSegment.text("text")])

使用消息模板

为了提供安全可靠的跨平台模板字符, 我们提供了一个消息模板功能来构建消息序列

它在以下常见场景中尤其有用:

  • 多行富文本编排(包含图片,文字以及表情等)

  • 客制化(由 Bot 最终用户提供消息模板时)

在事实上, 它的用法和str.format极为相近, 所以你在使用的时候, 总是可以参考Python 文档来达到你想要的效果

这里给出几个简单的例子:

:::tip 这里面所有的Message均是用对应 Adapter 的实现导入的, 而不是抽象基类 :::

>>> Message.template("{} {}").format("hello", "world")
Message(
    MessageSegment.text("hello"),
    MessageSegment.text(" "),
    MessageSegment.text("world")
)
>>> Message.template("{}{}").format(MessageSegment.image("file:///..."), "world")
Message(
    MessageSegment(type='image', data={'file': 'file:///...'}),
    MessageSegment(type='text', data={'text': 'world'})
)
>>> Message.template(
...     MessageSegment.text('{user_id}') + MessageSegment.face(233) +
...     MessageSegment.text('{message}')
... ).format_map({'user_id':123456, 'message':'hello world'}
...
Message(
    MessageSegment(type='text', data={'text': '123456'}),
    MessageSegment(type='face', data={'face': 233}),
    MessageSegment(type='text', data={'text': 'hello world'})
)
>>> Message.template("{link:image}").format(link='https://...')
Message(MessageSegment(type='image', data={'file': 'https://...'}))