# OneBot 消息操作

```{admonition} 相关知识
:class: note
如果你不知道什么是 OneBot “行为”和“行为操作”，建议先浏览：[行为的相关知识](../ob_refer/event-action)
```

消息操作作为 melobot 中最主要的行为操作，十分常用。

## 单条消息的构造

一般来说，发送纯文本内容是最普遍的，可以直接使用 melobot 的通用文本输出接口：

```python
from melobot import send_text
from melobot.protocols.onebot.v11 import on_message

@on_message(...)
async def _():
    await send_text("你好啊")
```

也可以使用具体协议的适配器的接口：

```python
from melobot.protocols.onebot.v11 import Adapter, on_message

@on_message(...)
async def _(adapter: Adapter):
    await adapter.send("你好啊")
```

如果要发送多媒体内容，则只能使用适配器的 {meth}`~.v11.Adapter.send` 接口。首先构造**消息段对象**，然后传入 {meth}`~.v11.Adapter.send` 作为参数。例如：

```python
from melobot.protocols.onebot.v11 import Adapter, on_message, ImageSegment

@on_message(...)
async def _(adapter: Adapter):
    # 构造一个“图片”消息段，然后发送
    img = ImageSegment(file="https://www.glowmem.com/static/avatar.jpg")
    await adapter.send(img)
```

或者传递原始的 OneBot v11 消息段字典：

```python
from melobot.protocols.onebot.v11 import Adapter, on_message

@on_message(...)
async def _(adapter: Adapter):
    # 构造一个“图片”消息段字典，然后发送
    img = {
        "type": "image",
        "data": {
            "url": "https://www.glowmem.com/static/avatar.jpg"
        }
    }
    await adapter.send(img)
```

其他消息段参考：[消息段对象](onebot_v11_segment)

单条消息中，自然可能有多种类型的消息段同时存在。此时这样处理：

```python
from melobot.protocols.onebot.v11 import Adapter, on_message, ImageSegment, TextSegment

@on_message(...)
async def _():
    # 例如文本和图片同时存在：
    await send([
        TextSegment("给你分享一张图片哦，这是 melobot 项目作者的头像"),
        ImageSegment(file="https://www.glowmem.com/static/avatar.jpg")
    ])
```

## 消息段的继承关系

在文档中，你会发现诸如 {class}`~.v11.adapter.segment.ImageSegment` 这样的类型，还会存在子类（{class}`~.v11.adapter.segment.ImageSendSegment` 和 {class}`~.v11.adapter.segment.ImageRecvSegment`）。这是为了提供更加精准的类型注解而产生的。所有这些类型只要文档中有 `__init__` 方法，即可手动实例化并使用。

## 自定义消息段的构造

通过 {class}`~.v11.adapter.segment.Segment` 创建**自定义消息段**。

例如在某些 OneBot 实现端，支持一种自定义的消息段：touch 消息（戳一戳，双击头像）。对应的消息段数据结构如下：

```json
{
    "type": "touch",
    "data": {
        "id": "1574260633"
    }
}
```

如何让 melobot 发送这种自定义的消息段？非常简单：

```python
from melobot.protocols.onebot.v11 import Segment
# 临时构造自定义消息段
seg = Segment(type="touch", id="1574260633")
```

或者构造一种新的消息段类型：

```python
from melobot.protocols.onebot.v11 import CustomSegCls, Segment
from typing_extensions import Literal, TypedDict

class _TouchData(TypedDict):
    id: str

# 返回新类型 TouchSegment
# 后续事件的消息段创建过程，会自动将对应消息段初始化为 TouchSegment 类型
TouchSegment: type[CustomSegCls[Literal['touch'], _TouchData]] = \
    Segment.add_type(Literal['touch'], _TouchData)
# 使用新的消息段类型来构造：
seg = TouchSegment(id="1574260633")
# 对象的这些属性拥有类型注解：
s.type  # Literal['touch']
s.data  # _TouchData
```

## 单条消息的其他发送方法

{meth}`~.v11.Adapter.send` 可根据当前触发事件，自动定位向何处发送消息。如果想要自定义发送目标，也很容易。只需要将 {meth}`~.v11.Adapter.send` 换成 {meth}`~.v11.Adapter.send_custom` 即可，它的第一参数与 {meth}`~.v11.Adapter.send_custom` 完全相同。

```python
from melobot.protocols.onebot.v11 import Adapter, on_message

@on_message(...)
async def _(adapter: Adapter):
    # 发送一个自定义目标的私聊消息，user_id 为 qq 号
    await adapter.send_custom(..., user_id=1574260633)
    # 发送一个自定义目标的群聊消息，group_id 为群号
    await adapter.send_custom(..., group_id=535705163)
```

## 获取 CQ 字符串

除使用消息段对象外，也可以使用**CQ 字符串**直接表示单条消息的所有消息内容。但只能从消息段对象生成 cq 字符串：

```python
from melobot.protocols.onebot.v11 import ImageSegment

img_cq: str = ImageSegment(file="https://example.com/test.jpg").to_cq()
```

```{admonition} 警告
:class: attention
CQ 字符串存在注入攻击的安全隐患。因此 melobot 不提供将 cq 字符串转为消息段的方法，也不允许接口直接发送 cq 字符串。
```

## 转发消息的构造

```{admonition} 相关知识
:class: note
如果你不知道转发消息的表示，主要依托于转发消息段和消息结点，建议先浏览：[转发消息与消息结点](../ob_refer/forward-msg.md)
```

### 转发消息段构造

构造转发消息段：

```python
from melobot.protocols.onebot.v11 import ForwardSegment

# forward_id 是转发 id，可通过消息事件的 get_datas("forward", "id") 获得
seg = ForwardSegment(forward_id)
```

此时，`seg` 变量已经是一条转发消息的等价表达了，直接使用适配器的 {meth}`~.v11.Adapter.send` 或 {meth}`~.v11.Adapter.send_custom` 发送即可。

### 消息结点构造

构造合并转发结点：

```python
from melobot.protocols.onebot.v11 import NodeSegment, NodeReferSegment

# 这里的 msg_id 是已存在的消息的 id，可通过消息事件的 id 获得
refer_node = NodeSegment(id=msg_id)

# 等价的从子类构造形式，拥有更好的语义：
refer_node = NodeReferSegment(id=msg_id)
```

构造合并转发自定义结点：

```python
from melobot.protocols.onebot.v11 import NodeSegment, NodeGocqCustomSegment, \
    NodeStdCustomSegment

# content 是消息内容，与上述消息段发送方法（例如 send, send_custom）的第一参数相同
# 后续参数是在转发消息中显示的，发送人昵称 和 发送人的qq号（int 类型）
node1 = NodeSegment(content="你好", name="melobot instance", uin=10001)

node2 = NodeSegment(
    content=ImageSegment(...),
    name="melobot instance",
    uin=10001
)

node3 = NodeSegment(
    content=[TextSegment(...), ImageSegment(...)],
    name="melobot instance",
    uin=10001
)

# 以上方法是按照 go-cq 风格构造的，如果需要使用 onebot v11 标准规定的格式：
node4 = NodeSegment(content=..., name=..., uin=..., use_std=True)

# 等价的从子类构造 go-cq 风格的：（参数名稍有不同，可自行查阅 API 文档）
node5 = NodeGocqCustomSegment(...)
# 等价的从子类构造 标准 风格的：（参数名稍有不同，可自行查阅 API 文档）
node6 = NodeStdCustomSegment(...)
```

将消息结点组成列表，就是一条转发消息的等价表达了，使用 {meth}`~.v11.Adapter.send_forward` 来发送它：

```python
from melobot.protocols.onebot.v11 import Adapter, on_message

@on_message(...)
async def _(adapter: Adapter):
    await adapter.send_forward([refer_node, node1, node2, node3])
```

## 转发消息的其他发送方法

{meth}`~.v11.Adapter.send_forward` 可根据当前触发事件，自动定位要向何处发送消息。同理，要自定义发送目标，将 {meth}`~.v11.Adapter.send_forward` 换成 {meth}`~.v11.Adapter.send_forward_custom` 即可。

```python
from melobot.protocols.onebot.v11 import Adapter, on_message

@on_message(...)
async def _(adapter: Adapter):
    # 发送一个自定义目标的私聊转发消息，user_id 为 qq 号
    await adapter.send_forward_custom(..., user_id=1574260633)
    # 发送一个自定义目标的群聊转发消息，group_id 为群号
    await adapter.send_forward_custom(..., group_id=535705163)
```

## 总结

本篇主要说明了如何构造和发送各种消息。

下一篇将重点说明：其他行为操作及行为操作的等待与响应。
