# Drafty: 富消息格式 Drafty 是 Tinode 用于消息样式的文本格式。Drafty 的设计目标是足够表达力,同时避免过多的安全风险。可以将其视为 JSON 封装的 [markdown](https://en.wikipedia.org/wiki/Markdown)。Drafty 受到 FB 的 [draft.js](https://draftjs.org/) 规范影响。目前已有 [Javascript](https://github.com/tinode/tinode-js/blob/master/src/drafty.js)、[Java](https://github.com/tinode/tindroid/blob/master/tinodesdk/src/main/java/co/tinode/tinodesdk/model/Drafty.java) 和 [Swift](https://github.com/tinode/ios/blob/master/TinodeSDK/model/Drafty.swift) 实现。[Go 实现](https://github.com/tinode/chat/blob/master/server/drafty/drafty.go) 可将 Drafty 转换为纯文本和预览。 ## 示例 > this is **bold**, `code` and _italic_, ~~strike~~
> combined **bold and _italic_**
> an url: https://www.example.com/abc#fragment and another _[https://web.tinode.co](https://web.tinode.co)_
> this is a [@mention](#) and a [#hashtag](#) in a string
> second [#hashtag](#)
上述文本的 Drafty-JSON 表示: ```js { "txt": "this is bold, code and italic, strike combined bold and italic an url: https://www.example.com/abc#fragment and another www.tinode.co this is a @mention and a #hashtag in a string second #hashtag", "fmt": [ { "at":8, "len":4,"tp":"ST" },{ "at":14, "len":4, "tp":"CO" },{ "at":23, "len":6, "tp":"EM"}, { "at":31, "len":6, "tp":"DL" },{ "tp":"BR", "len":1, "at":37 },{ "at":56, "len":6, "tp":"EM" }, { "at":47, "len":15, "tp":"ST" },{ "tp":"BR", "len":1, "at":62 },{ "at":120, "len":13, "tp":"EM" }, { "at":71, "len":36, "key":0 },{ "at":120, "len":13, "key":1 },{ "tp":"BR", "len":1, "at":133 }, { "at":144, "len":8, "key":2 },{ "at":159, "len":8, "key":3 },{ "tp":"BR", "len":1, "at":179 }, { "at":187, "len":8, "key":3 },{ "tp":"BR", "len":1, "at":195 } ], "ent": [ { "tp":"LN", "data":{ "url":"https://www.example.com/abc#fragment" } }, { "tp":"LN", "data":{ "url":"http://www.tinode.co" } }, { "tp":"MN", "data":{ "val":"mention" } }, { "tp":"HT", "data":{ "val":"hashtag" } } ] } ``` ## 结构 Drafty 对象有三个字段:纯文本 `txt`、内联标记 `fmt` 和实体 `ent`。 ### 纯文本 要发送的消息转换为纯 Unicode 文本,所有标记被剥离并保存在 `txt` 字段。通常,有效的 Drafty 可以只包含 `txt` 字段。 ### 内联格式 `fmt` 内联格式是 `fmt` 字段中的样式数组。每个样式由至少包含 `at` 和 `len` 字段的对象表示。`at` 值表示 `txt` 中从 0 开始的偏移量,`len` 是要应用格式的字符数。样式的第三个值是 `tp` 或 `key`。 如果提供 `tp`,表示样式是基本文本装饰: * `BR`: 换行。 * `CO`: 代码或等宽文本,可能有不同背景:`monotype`。 * `DL`: 删除或删除线文本:~~strikethrough~~。 * `EM`: 强调文本,通常表示为斜体:_italic_。 * `FM`: 表单/字段集;也可表示为实体。 * `HD`: 隐藏内容。 * `HL`: 高亮文本,如不同颜色或背景的文本;无法指定颜色。 * `RW`: 格式的逻辑分组,行;也可表示为实体。 * `ST`: 强调或粗体文本:**bold**。 如果提供 key,它是 `ent` 数组的 0 基索引,包含扩展样式参数(如图片或 URL): * `AU`: 嵌入音频。 * `BN`: 交互按钮。 * `EX`: 通用附件。 * `FM`: 表单/字段集;也可表示为基本装饰。 * `HT`: 话题标签,如 [#hashtag](#)。 * `IM`: 内联图片。 * `LN`: 链接(URL)[https://api.tinode.co](https://api.tinode.co)。 * `MN`: 提及,如 [@tinode](#)。 * `RW`: 格式的逻辑分组,行;也可表示为基本装饰。 * `VC`: 视频(和音频)通话。 * `VD`: 内联视频。 示例: * `{ "at":8, "len":4, "tp":"ST"}`: 从 `txt` 偏移 8 开始对 4 个字符应用 `ST`(粗体)格式。 * `{ "at":144, "len":8, "key":2 }`: 在位置 144 插入实体 `ent[2]`,实体跨越 8 个字符。 * `{ "at":-1, "len":0, "key":4 }`: 将 `ent[4]` 显示为文件附件,不应用任何文本样式。 客户端应能处理缺失的 `at`、`key` 和 `len` 值。缺失值假定为 `0`。 索引 `at` 和 `len` 以 [Unicode 码点](https://developer.mozilla.org/en-US/docs/Glossary/Code_point)计算,而非字节或字符。带有 Fitzpatrick 肤调修饰符、变体选择器或用 `ZWJ` 分组等多码点字形的行为目前未定义。 #### `FM`: 表单,有序字段集 表单提供了为逻辑元素组添加段落级格式的方法。它可以表示为文本装饰或实体。
Do you agree?
Yes
No
```js { "txt": "Do you agree? Yes No", "fmt": [ {"len": 20, "tp": "FM"}, // 缺失 'at' 为零: "at": 0 {"len": 13, "tp": "ST"} {"at": 13, "len": 1, "tp": "BR"}, {"at": 14, "len": 3}, // 缺失 'key' 为零: "key": 0 {"at": 17, "len": 1, "tp": "BR"}, {"at": 18, "len": 2, "key": 1}, ], "ent": [ {"tp": "BN", "data": {"name": "yes", "act": "pub", "val": "oh yes!"}}, {"tp": "BN", "data": {"name": "no", "act": "pub"}} ] } ``` 如果上例中按了 `Yes` 按钮,客户端应向服务器发送以下内容的消息: ```js { "txt": "Yes", "fmt": [{ "at":-1 }], "ent": [{ "tp": "EX", "data": { "mime": "text/x-drafty-fr", // drafty form-response. "val": { "seq": 15, // 包含表单的消息 seq id. "resp": {"yes": "oh yes!"} } } }] } ``` 表单也可表示为实体: ```js { "tp": "FM", "data": { "su": true } } ``` `data.su` 描述点击后交互表单元素的行为。`"su": true` 表示表单是`单次使用`:首次交互后表单应更改以显示不再接受输入。 ### 实体 `ent` 通常,实体是需要额外(可能很大)数据的文本装饰。实体由包含两个字段的物件表示:`tp` 表示实体类型,`data` 是类型相关的样式信息。未知字段被忽略。 #### `AU`: 嵌入音频记录 `AU` 是音频记录。`data` 包含以下字段: ```js { "tp": "AU", "data": { "mime": "audio/aac", "val": "Rt53jUU...iVBORw0KGgoA==", "ref": "/v0/file/s/e769gvt1ILE.m4v", "preview": "Aw4JKBkAAAAKMSM...vHxgcJhsgESAY" "duration": 180000, "name": "ding_dong.m4a", "size": 595496 } } ``` * `mime`: 数据类型,如 'audio/ogg'。 * `val`: 可选带内音频数据:base64 编码的音频位。 * `ref`: 可选带外音频数据引用。必须有 `val` 或 `ref`。 * `preview`: base64 编码的字节数组,用于生成可视化预览;每个字节是一个振幅条。 * `duration`: 录音时长(毫秒)。 * `name`: 原始文件的可选名称。 * `size`: 文件大小(字节)的可选值。 创建只包含单个音频记录、无文本的消息,使用以下 Drafty: ```js { txt: " ", fmt: [{len: 1}], ent: [{tp: "AU", data: {<你的音频数据>}}] } ``` _重要安全考虑_:`val` 和 `ref` 字段可能包含恶意载荷。客户端应将 `ref` 字段的 URL 方案限制为仅 `http` 和 `https`。客户端仅当正确转换为音频时,才应向用户展示 `val` 字段内容。 #### `BN`: 交互按钮 `BN` 提供向服务器发送数据的选项,可以是源服务器或其他服务器。`data` 包含以下字段: ```js { "tp": "BN", "data": { "name": "confirmation", "act": "url", "val": "some-value", "ref": "https://www.example.com/path/?foo=bar" } } ``` * `act`: 按钮点击响应的动作类型: * `pub`: 向当前话题发送 Drafty 格式的 `{pub}` 消息,表单数据作为附件: ```js { "tp":"EX", "data":{ "mime":"text/x-drafty-fr", "val": { "seq": 3, "resp": { "confirmation": "some-value" } } } } ``` * `url`: 向 `data.ref` 字段的 URL 发起 `HTTP GET` 请求。以下查询参数附加到 URL:`=`、`uid=<当前用户ID>`、`topic=<话题名>`、`seq=<消息序列ID>`。 * `note`: 向当前话题发送 `{note}` 消息,`what` 设为 `data`(暂未实现,需要请联系我们)。 ```js { "what": "data", "data": { "mime": "text/x-drafty-fr", "val": { "seq": 3, "resp": { "confirmation": "some-value" } } } ``` * `name`: 按钮的可选名称,将回传给服务器。 * `val`: 额外的不透明数据。 * `ref`: `url` 动作的 URL。 如果提供了 `name` 但没有 `val`,`val` 假定为 `1`。如果 `name` 未定义,则 `name` 和 `val` 都不发送。 上例按钮将发送 HTTP GET 到 https://www.example.com/path/?foo=bar&confirmation=some-value&uid=usrFsk73jYRR&topic=grpnG99YhENiQU&seq=3(假设当前用户 ID 为 `usrFsk73jYRR`,话题为 `grpnG99YhENiQU`,含按钮消息的序列 ID 为 `3`)。 _重要安全考虑_:客户端应将 `ref` 字段的 URL 方案限制为仅 `http` 和 `https`。 #### `EX`: 文件附件 `EX` 是客户端不应尝试解释的附件。`data` 包含以下字段: ```js { "tp": "EX", "data": { "mime", "text/plain", "val", "Q3l0aG9uPT0w...PT00LjAuMAo=", "ref": "/v0/file/s/abcdef12345.txt", "name", "requirements.txt", "size": 1234 } } ``` * `mime`: 数据类型,如 'application/octet-stream'。 * `val`: 可选带内 base64 编码文件数据。 * `ref`: 可选带外文件数据引用。必须有 `val` 或 `ref`。 * `name`: 原始文件的可选名称。 * `size`: 文件大小(字节)的可选值。 生成将文件附件显示为可下载文件的消息,使用以下格式: ```js { at: -1, len: 0, key: } ``` _重要安全考虑_:`ref` 字段可能包含恶意载荷。客户端应将 `ref` 字段的 URL 方案限制为仅 `http` 和 `https`。 #### `IM`: 内联图片或带内联预览的附加图片 `IM` 是图片。`data` 包含以下字段: ```js { "tp": "IM", "data": { "mime": "image/png", "val": "Rt53jUU...iVBORw0KGgoA==", "ref": "/v0/file/s/abcdef12345.jpg", "width": 512, "height": 512, "name": "sample_image.png", "size": 123456 } } ``` * `mime`: 数据类型,如 'image/jpeg'。 * `val`: 可选带内图片数据:base64 编码的图片位。 * `ref`: 可选带外图片数据引用。必须有 `val` 或 `ref`。 * `width`, `height`: 图片的线性尺寸(像素)。 * `name`: 原始文件的可选名称。 * `size`: 文件大小(字节)的可选值。 创建只包含单个图片、无文本的消息,使用以下 Drafty: ```js { txt: " ", fmt: [{len: 1}], ent: [{tp: "IM", data: {<你的图片数据>}}] } ``` _重要安全考虑_:`val` 和 `ref` 字段可能包含恶意载荷。客户端应将 `ref` 字段的 URL 方案限制为仅 `http` 和 `https`。客户端仅当正确转换为图片时,才应向用户展示 `val` 字段内容。 #### `LN`: 链接(URL) `LN` 是 URL。`data` 包含单个 `url` 字段: `{ "tp": "LN", "data": { "url": "https://www.example.com/abc#fragment" } }` `url` 可以是客户端知道如何解释的任何有效 URL,例如也可以是邮件或电话 URL:`email:alice@example.com` 或 `tel:+17025550001`。 _重要安全考虑_:`url` 字段可能是恶意构造的。客户端应禁用某些 URL 方案,如 `javascript:` 和 `data:`。 #### `MN`: 提及,如 [@alice](#) 提及 `data` 包含单个 `val` 字段,值为被提及用户的 ID: ```js { "tp":"MN", "data":{ "val":"usrFsk73jYRR" } } ``` #### `HT`: 话题标签,如 [#tinode](#) 话题标签 `data` 包含单个 `val` 字段,值为客户端软件需要解释的话题标签值,例如可以是搜索词: ```js { "tp":"HT", "data":{ "val":"tinode" } } ``` #### `VC`: 视频通话控制消息 视频通话 `data` 包含通话当前状态和时长: ```js { "tp": "VC", "data": { "duration": 10000, "state": "disconnected", "incoming": false, "aonly": true } } ``` * `duration`: 通话时长(毫秒)。 * `state`: 当前通话状态;支持的状态: * `accepted`: 通话已建立(进行中)。 * `busy`: 通话无法建立,因为被叫方正忙于另一通电话。 * `finished`: 之前建立的通话已成功结束。 * `disconnected`: 通话断开,例如因错误。 * `missed`: 未接来电,即被叫方未接电话。 * `declined`: 拒接来电,即被叫方在接听前挂断。 * `incoming`: 如果是来电则为 true,否则是去电。 * `aonly`: 如果是纯音频通话(无视频)则为 true。 `VC` 也可表示为格式 `"fmt": [{"len": 1, "tp": "VC"}]`,无实体。这种情况下,所有通话信息包含在消息的 `head` 字段中。 #### `VD`: 带预览的视频 `VD` 表示视频录像。`data` 包含以下字段: ```js { "tp": "VD", "data": { "mime": "video/webm", "ref": "/v0/file/s/abcdef12345.webm", "preview": "AsTrsU...k86n00Ggo==" "preref": "/v0/file/s/abcdef54321.jpeg", "premime": "image/jpeg", "width": 640, "height": 360, "duration": 32000, "name": " bigbuckbunny.webm", "size": 1234567 } } ``` * `mime`: 视频数据类型,如 'video/webm'。 * `val`: 可选带内视频数据:base64 编码的视频位,通常不存在(null)。 * `ref`: 可选带外视频数据引用。必须有 `val` 或 `ref`。 * `preview`: 可选 base64 编码的视频截图图像(海报)。 * `preref`: 可选带外视频截图图像(海报)引用。 * `premime`: 可选截图图像(海报)的数据类型;缺失时假定为 'image/jpeg'。 * `width`, `height`: 视频和海报的线性尺寸(像素)。 * `duration`: 视频时长(毫秒)。 * `name`: 原始文件的可选名称。 * `size`: 文件大小(字节)的可选值。 创建只包含单个视频、无文本的消息,使用以下 Drafty: ```js { txt: " ", fmt: [{len: 1}], ent: [{tp: "VD", data: {<你的视频数据>}}] } ``` _重要安全考虑_:`val`、`ref`、`preview` 字段可能包含恶意载荷。客户端应将 `ref` 和 `preview` 字段的 URL 方案限制为仅 `http` 和 `https`。客户端仅当正确转换为视频时,才应向用户展示 `val` 字段内容。