Skip to main content

消息链

为什么是消息链?

QQ 消息并不只是纯文本,也不只是单一类型的消息。文本中可以夹杂着图片、At 某人等多种类型的消息。

mirai 为了处理富文本消息,采用了消息链(Message Chain)这一方式。

消息链可以看作是一系列消息组件(Message Component)构成的列表。消息组件表示消息中的一部分,比如纯文本 Plain,At 某人 At 等等。

关于可用的消息组件,参看 API 文档

构造消息链

一个构造消息链的例子:

message_chain = MessageChain([
AtAll(),
Plain("Hello World!"),
])

Plain 可以省略。

message_chain = MessageChain([
AtAll(),
"Hello World!",
])

使用消息链

在调用 API 时,参数中需要 MessageChain 的,也可以使用 List[MessageComponent] 代替。 例如,以下两种写法是等价的:

await bot.send_friend_message(12345678, [
Plain("Hello World!")
])
await bot.send_friend_message(12345678, MessageChain([
Plain("Hello World!")
]))

消息链的字符串表示

使用 str(message_chain) 获取消息链的字符串表示。字符串表示的格式类似于手机 QQ 在通知栏消息中的格式,例如图片会被转化为 [图片],等等。

message_chain.as_mirai_code() 返回消息的 mirai 码格式,并自动按照 mirai 码的规定转义。参看 mirai 的文档

在消息链上的操作

遍历

可以使用 for 循环遍历消息链中的消息组件。

for component in message_chain:
print(repr(component))

比较

可以使用 == 运算符比较两个消息链是否相同。

another_msg_chain = MessageChain([
{
"type": "AtAll"
}, {
"type": "Plain",
"text": "Hello World!"
},
])
print(message_chain == another_msg_chain)
'True'

检查子链

可以使用 in 运算检查消息链中:

  1. 是否有某个消息组件。
  2. 是否有某个类型的消息组件。
  3. 是否有某个子消息链。
  4. 对应的 mirai 码中是否有某子字符串。
if AtAll in message_chain:
print('AtAll')

if At(bot.qq) in message_chain:
print('At Me')

if MessageChain([At(bot.qq), Plain('Hello!')]) in message_chain:
print('Hello!')

if 'Hello' in message_chain:
print('Hi!')

消息链的 has 方法和 in 等价。

if message_chain.has(AtAll):
print('AtAll')

也可以使用 >=<= 运算符:

if MessageChain([At(bot.qq), Plain('Hello!')]) <= message_chain:
print('Hello!')
tip

此处的子消息链匹配会把 Plain 看成一个整体,而不是匹配其文本的一部分。

如需对文本进行部分匹配,请采用 mirai 码字符串匹配的方式。

索引与切片

消息链对索引操作进行了增强。以消息组件类型为索引,获取消息链中的全部该类型的消息组件。

plain_list = message_chain[Plain]
'[Plain("Hello World!")]'

类型, 数量 为索引,获取前至多多少个该类型的消息组件。

plain_list_first = message_chain[Plain, 1]
'[Plain("Hello World!")]'

消息链的 get 方法和索引操作等价。

plain_list_first = message_chain.get(Plain)
'[Plain("Hello World!")]'

消息链的 get 方法还可指定第二个参数 count,这相当于以 类型, 数量 为索引。

plain_list_first = message_chain.get(Plain, 1)
# 这等价于
plain_list_first = message_chain[Plain, 1]

连接与复制

可以用加号连接两个消息链。

MessageChain(['Hello World!']) + MessageChain(['Goodbye World!'])
# 返回 MessageChain([Plain("Hello World!"), Plain("Goodbye World!")])

可以用 * 运算符复制消息链。

MessageChain(['Hello World!']) * 2
# 返回 MessageChain([Plain("Hello World!"), Plain("Hello World!")])

其他

除此之外,消息链还支持很多 list 拥有的操作,比如 indexcount

message_chain = MessageChain([
AtAll(),
"Hello World!",
])
message_chain.index(Plain)
# 返回 0
message_chain.count(Plain)
# 返回 1

消息链对这些操作进行了拓展。在传入元素的地方,一般都可以传入元素的类型。

使用 Mirai 码

MiraiCode 消息组件表示 mirai 码,可以用于构建消息链。关于 mirai 码,请参阅 mirai 的文档

from mirai import MiraiCode
message_chain = MiraiCode('[mirai:at:123456789]')

图片与语音

接收图片

获取消息链的 Image 元素后,可以通过其 download 方法下载图片。

images = message_chain[Image]
for image in images:
await image.download(directory='./images')

下载时,可以指定文件名或目录名。

await image.download(directory='./images')
await image.download(filename='./images/1.png')

默认情况下,文件名的拓展名会被忽略,由接收到的图片的类型决定拓展名。可以通过 determine_type=False 禁用这一行为。

await image.download(filename='./images/1.png', determine_type=False)

接收语音

语音的下载与图片基本相同,都是通过 download 方法实现。

voice = message_chain[Voice][0] # 一条消息只会包含一个语音
await voice.download(directory='./voices')
# 或者指定文件名
await image.download(filename='./voices/1.silk')

语音采用 silk v3 格式编码,silk 格式的编解码请使用 graiax-silkcoder

发送图片

发送图片的最简单的方式是直接实例化 Image 类型的消息组件。此方式支持本地图片、网络图片和 base64 编码的图片。

from pathlib import Path
message_chain = MessageChain([
Image(path=str('./images/1.png')),
Image(url='https://raw.githubusercontent.com/mamoe/mirai/dev/docs/mirai.png'),
Image(base64='/9j/4AAQSkZJRgABAQEAYABgAAD/2wBDAAgGBgcGBQgHBwcJCQgKDBQNDAsLDBkS' \
+ 'Ew8UHRofHh0aHBwgJC4nICIsIxwcKDcpLDAxNDQ0Hyc5PTgyPC4zNDL/2wBDAQkJ' \
+ 'CQwLDBgNDRgyIRwhMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIyMjIy' \
+ 'MjIyMjIyMjIyMjIyMjL/wgARCAAbABsDASIAAhEBAxEB/8QAGQAAAgMBAAAAAAAA' \
+ 'AAAAAAAAAAQBBQYD/8QAGAEAAgMAAAAAAAAAAAAAAAAAAAECAwT/2gAMAwEAAhAD' \
+ 'EAAAAd+l2zry6kQfV8VloCTcAl//xAAdEAACAgEFAAAAAAAAAAAAAAABAwACBBAR' \
+ 'EiAj/9oACAEBAAEFApbKXW+jiapxVBrVu9oRuDi8YlIV2//EABkRAQADAQEAAAAA' \
+ 'AAAAAAAAAAIAAQMxEf/aAAgBAwEBPwHfVFUB24XS5FmV2E0a8qf/xAAVEQEBAAAA' \
+ 'AAAAAAAAAAAAAAABIP/aAAgBAgEBPwEj/8QAIRAAAQIEBwAAAAAAAAAAAAAAAQAR' \
+ 'ECExQQIDEiAjQmH/2gAIAQEABj8CWip8jiIqymWaaOTVrwIN1xonsd3/xAAcEAAB' \
+ 'BAMBAAAAAAAAAAAAAAABABEhMRBBYSD/2gAIAQEAAT8hQmDnLcZpWQTDOxS1JsUU' \
+ '6AMVto7afEDaR6//2gAMAwEAAgADAAAAEKf3/P/EABkRAAMAAwAAAAAAAAAAAAAA' \
+ 'AAABIRFBgf/aAAgBAwEBPxBNgKW9J0srUY1w/8QAFhEBAQEAAAAAAAAAAAAAAAAA' \
+ 'AQAR/9oACAECAQE/EAJrJkKS7f/EAB4QAQACAgMAAwAAAAAAAAAAAAERIQBREDFB' \
+ 'IGFx/9oACAEBAAE/EMY4i4EJYt5c2Eq07xBRBOyJ1iLC5tzqCn784HiUDY54k9Di' \
+ 'X6ZeQrEwaPl//9k=')
])
路径问题

Image 组件的 path 属性中,相对路径的含义至今仍有不明确之处(#409)。

在 YiriMirai 0.2.0 之后,将 path 属性的含义改为相对路径基于 YiriMirai 的当前目录。这一行为和 mirai-api-http 的行为有所不同,请注意

同样地,下面说的 Voice 组件的 path 属性采用一样的处理方法。

此外,还有几种发送图片的方法。

使用 Image.from_local,将从本地读取图片,并以 base64 编码的形式发送。此方式可能会消耗较多内存。

message_chain = MessageChain([
await Image.from_local('./images/1.png')
])

使用 upload_image API 将图片提前上传到服务器。

message_chain = MessageChain([
# 第一个参数为会话类型,可以为 'friend' 'group' 或 'temp'
await bot.upload_image('friend', './images/1.png')
])
caution

upload_image API 只能在 HTTP 适配器下工作(包括下文提到的 upload_voice)。这是由于 WebSocket 在文件传输时会阻塞管道,造成信号延迟。

如果要在 WebSocket 适配器下上传图片或语音,可以使用 bot.use_adapter,临时启用 HTTP 适配器。

async with bot.use_adapter(HTTPAdapter.via(bot)):
image = await bot.upload_image('friend', './images/1.png')

发送语音

语音的发送与图片类似。此方式支持本地语音、网络语音文件和 base64 编码的语音。

from pathlib import Path
message_chain1 = MessageChain([
Voice(path=str('./voices/1.silk'))
])
message_chain2 = MessageChain([Voice(url='...')])
message_chain3 = MessageChain([Voice(base64='...')])

语音消息除包含一个 Voice 组件外,不能再包含其他组件。

私聊语音

由于目前 mirai-api-http 只支持群聊语音,不支持私聊语音,在私聊中发送语音会导致未知的错误。

此外,和图片一样,可以通过 Voice.from_local 将本地语音以 base64 的形式发送,也可以通过 upload_voice API 提前上传语音到服务器(需要 HTTP 适配器)。

message_chain4 = MessageChain([
await Voice.from_local('./voices/1.silk')
])
message_chain5 = MessageChain([
# 第一个参数为会话类型,只能为 'group'
await bot.upload_voice('group', './voices/1.silk')
])