# 消息元素

> For the complete documentation index, see [llms.txt](https://simbot.forte.love/llms.txt).

> **TL;DR**
> * 对那些 核心库 中、实现了 simbot 标准库的 `Message.Element` 消息元素类型的说明，
>
> * 对一些与 核心库 中“消息”相关的内容的补充说明。

## See also

### 相关链接

[消息元素与消息链](basic-messages.html)

> **Note:**
> 有关消息元素、消息链的更多信息，可参考 [消息元素与消息链](basic-messages.html) 。

## 消息元素实现

所有的 `Message.Element` 特殊实现类型均定义在包 `love.forte.simbot.component.qguild.message` 中。

它们都继承了 `love.forte.simbot.component.qguild.message.QGMessageElement` 。

QGArk
: 对 API 模块中 Ark 消息的包装体，可用来发送 `Ark` 消息。

QGContentText

QGMarkdown
: > **Tip:**
: > 仅用于发送。添加自 `4.0.0-beta6` 。

QGAttachmentMessage
: 接收到的附件消息，字段说明见下文的附件章节。

QGEmbed
: 对 API 模块中 Embed 消息的包装体，可用来发送 `Embed` 消息。

QGReference
: 发送消息时，QQ频道的消息引用。与官方发送消息API中的 `reference` 对应。

QGReplyTo
: > **Tip:**
: > 仅用于发送。
:
:
:
: 发送消息时，指定一个需要回复的目标消息ID。

QGMedia
: > **Tip:**
: > 仅用于发送。添加自 `4.0.0-beta6` 。

QGKeyboards
: > **Tip:**
: > 仅用于群聊/单聊发送。添加自 `4.4.0` 。
:
:
:
: 对 API 模块中 `MessageKeyboards` 的包装体，可用来发送包含多行、多按钮的 Markdown 消息按钮。 旧的 `QGKeyboard` 只包装单个 `MessageKeyboard`，自 `4.4.0` 起应改用 `QGKeyboards`。

QGKeyboard
: > **Tip:**
: > 仅用于群聊/单聊发送。自 `4.4.0` 起废弃。
:
:
:
: 旧的单按钮包装体，只能包装单个 `MessageKeyboard`，无法完整表达官方 `keyboard.content.rows[].buttons[]` 结构。 请使用 `QGKeyboards` 替代。

## 附件

`QGAttachmentMessage` 用于接收附件，暂不支持直接发送。 `url` 为下载地址，缺少协议前缀时会补充 `https://`。

附件信息通过以下类型化字段提供。

| 字段 | 类型与含义 |
| --- | --- |
| `filename` | `String?`，文件名 |
| `width` / `height` | `Int?`，图片宽高，单位为像素 |
| `size` | `Long?`，文件大小，单位为字节 |
| `contentType` | `String?`，附件内容类型 |
| `voiceWavUrl` | `String?`，语音转换后的 WAV 地址 |
| `asrReferText` | `String?`，语音识别参考结果 |
| `attachmentId` | `String?`，QQ 附件 ID，需要选择加入 `QGUnstableProperty` |

`source` 提供原始的 `Message.Attachment`。其中的 `url` 可能没有协议前缀。 未由平台提供的可选属性为 `null`。

旧的 `properties` 字符串属性表、构造函数、`copy()` 和解构方法已废弃。 旧 `id` 也已废弃，其值仍为下载 URL 的 `ID` 包装；需要下载地址时使用 `url`，需要平台附件 ID 时使用 `attachmentId`。

序列化仍使用 `qg.attachment` 类型名，新格式输出类型化字段，反序列化兼容旧 `properties` 中的字段。 历史自定义字符串属性仍会保留。

## 发送消息

在simbot中，使用组件的消息元素与使用其他消息元素别无二致， 通常使用 `SendSupport` 和 `ReplySupport` 的实现类中提供的 `send(...)` 和 `reply(..)` API 发送消息。

前者多由 [行为对象](component-qq-guild-actors.html) 中的一些类型实现(例如`QGMember`、`QGTextChannel`)， 而后者则通常由与消息相关的事件实现(例如 `QGAtMessageCreateEvent`)。

此处以 `QGTextChannel` 为例，`send` 可以使用拼接后的消息链、字符串或单独的消息元素作为参数。

Kotlin:

```KOTLIN
val channel: QGTextChannel = ...
channel.send("消息内容")
channel.send("消息内容".toText() + At("user id".ID))
```

Java:

```JAVA
QGTextChannel channel = ...

var sendTask1 = channel.sendAsync("消息内容");
var sendTask2 = channel.sendAsync(Messages.of(
        Text.of("文本消息"),
        At.of(Identifies.of("user id"))
));
```

```JAVA
QGTextChannel channel = ...;

channel.sendBlocking("消息内容");
channel.sendBlocking(Messages.of(
        Text.of("文本消息"),
        At.of(Identifies.of("user id"))
    ));
```

```JAVA
QGTextChannel channel = ...;

channel.sendReserve("消息内容")
        .transform(SuspendReserves.mono())
        .subscribe(receipt -> { ... });

channel.sendReserve(Messages.of(
        Text.of("文本消息"),
        At.of(Identifies.of("user id"))
    ))
    .transform(SuspendReserves.mono())
    .subscribe(receipt -> { ... });
```

## 消息撤回

自 `5.0` 起，QQ 群消息事件的 `messageContent` 也支持撤回当前收到的群消息：

```KOTLIN
process<QGGroupMessageCreateEvent> { event ->
    event.messageContent.delete()
}
```

`QGGroupAtMessageCreateEvent` 的 `messageContent` 也具有相同能力。 C2C 单聊消息虽然同样提供 `messageContent`，但其中没有可用于撤回的目标消息上下文， 因此调用 `delete()` 时仍不支持撤回。频道消息使用频道消息撤回接口，具体参见 [DeleteChannelMessageApi](component-qq-guild-api-list.html#love_forte_simbot_qguild_api_message_DeleteChannelMessageApi)； 旧的 `DeleteMessageApi` 仅为兼容保留。

从 `QGGroup` 或 `QGFriend` 发送、回复消息得到的 `QGMessageReceipt` 会保留目标 OpenID， 因此可以撤回机器人刚发送的消息。消息链被拆分为多条消息时，聚合回执的 `delete()` 会依次撤回其中的消息：

```KOTLIN
val groupReceipt = group.send("临时提示")
groupReceipt.delete()

val friendReceipt = friend.send("C2C 临时消息")
friendReceipt.delete()
```

群消息与 C2C 单聊消息通常只能撤回发送时间不超过两分钟的消息。 群管理员可以撤回机器人自身和普通成员的消息，普通成员只能撤回机器人自身发送的消息； C2C 单聊撤回接口只能撤回机器人发送给用户的消息。

如果当前实现没有撤回上下文，默认会抛出 `UnsupportedOperationException`； 可以传入 `StandardDeleteOption.IGNORE_ON_UNSUPPORTED` 忽略不支持的场景。 QQ 服务端拒绝撤回时默认继续抛出异常，也可以使用 `StandardDeleteOption.IGNORE_ON_FAILURE` 忽略失败。

## 按目的地将普通文本作为 Markdown 发送

自 `4.5.0` 起，可以通过 [Bot 配置文件](component-qq-guild-bot-config.html) 的 `contentAsMarkdownAll` 和 `contentAsMarkdown`，分别为 `CHANNEL`、`DMS`、`GROUP`、`USER` 四种目的地控制普通文本的发送载荷。

启用后，普通 `Text`、文本消息链以及其中的 @、表情等文本内容会写入 Markdown 的 `content`。这不会替代显式构造 `QGMarkdown`：当你需要发送模板 Markdown 或自行指定 Markdown 内容时，仍应使用 `QGMarkdown`。

## Markdown 消息按钮

> **Tip:**
> 底层按钮模型的 `buttonStyle`、`actionType`、`actionAnchor` 和 `permissionType` 分别使用 `MessageKeyboardStyle`、`MessageKeyboardActionType`、`MessageKeyboardActionAnchor` 和 `MessageKeyboardActionPermissionType`。通过各类型的 `of(...)` 包装原始数值。 builder / DSL 中的 `style`、`type` 和 `anchor` 仍可接收原始数值。

自 `4.4.0` 起，群聊和单聊 Markdown 消息按钮使用 `QGKeyboards` / `MessageKeyboards` 表示。 它支持按行组织多个按钮，并在发送体中仍序列化为官方 API 的 `keyboard` 字段。

> **Warning:**
> 旧的 `QGKeyboard` 与直接使用 `MessageKeyboard` 的发送入口仅能表达单个按钮，且结构不完整无法直接被 API 使用，已经被废弃。
>
>
>
> `QGKeyboard` 自 `4.4.0` 起废弃，编译期会以 `ERROR` 级别提示。 原本使用 `QGKeyboard.create(...)` 的位置，应替换为 `QGKeyboards.create(...)` 或 `QGKeyboards { ... }`。

Kotlin:

```KOTLIN
val keyboards = QGKeyboards {
    content {
        row {
            button {
                renderData("确认", visitedLabel = "已确认", style = 1)
                action {
                    type = 1
                    data = "confirm"
                    unsupportTips = "当前客户端暂不支持"
                    permissionAllAccessible()
                }
            }
        }
        row {
            addButton(MessageKeyboard.create("template-id"))
        }
    }
}

group.send(QGMarkdown.create("请选择") + keyboards)

// 只有一行按钮时，也可以直接包装 MessageKeyboards。
val singleRow = MessageKeyboards.create(
    listOf(
        MessageKeyboard.create("template-a"),
        MessageKeyboard.create("template-b")
    )
)

group.send(QGMarkdown.create("请选择") + QGKeyboards.create(singleRow))
```

Java:

```JAVA
var keyboards = MessageKeyboards.create(List.of(
        MessageKeyboard.create("template-a"),
        MessageKeyboard.create("template-b")
));

var sendTask = group.sendAsync(Messages.of(
        QGMarkdown.create("请选择"),
        QGKeyboards.create(keyboards)
));
```

```JAVA
var keyboards = MessageKeyboards.create(List.of(
        MessageKeyboard.create("template-a"),
        MessageKeyboard.create("template-b")
));

group.sendBlocking(Messages.of(
        QGMarkdown.create("请选择"),
        QGKeyboards.create(keyboards)
));
```

```JAVA
var keyboards = MessageKeyboards.create(List.of(
        MessageKeyboard.create("template-a"),
        MessageKeyboard.create("template-b")
));

group.sendReserve(Messages.of(
        QGMarkdown.create("请选择"),
        QGKeyboards.create(keyboards)
)).transform(SuspendReserves.mono()).subscribe(receipt -> { ... });
```

