chore: seed migration objects batch 2
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 211 KiB |
|
Before Width: | Height: | Size: 0 B After Width: | Height: | Size: 755 KiB |
@@ -0,0 +1,108 @@
|
|||||||
|
# 视频通话建立流程
|
||||||
|
|
||||||
|
Tinode 通过 [WebRTC](https://webrtc.org/) 支持点对点视频通话。下图展示了两个用户 `Alice` 和 `Bob` 之间的通话建立流程。该流程概念上类似于 [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol),但使用原生 Tinode 消息传输。
|
||||||
|
|
||||||
|
注意:
|
||||||
|
- 所有通信都由 Tinode 服务器代理。
|
||||||
|
- 客户端到服务器事件通过 `{note}` 消息分发,设置通话的 `topic` 和 `seq` 字段。
|
||||||
|
- 服务器到客户端数据通过 `me` 话题的 `{info}` 消息路由,设置通话的 `src`(通话话题)和 `seq` 字段(和/或在数据推送通知中)。
|
||||||
|
- 假设 Alice 和 Bob 都可能有多个设备。
|
||||||
|
|
||||||
|
## 详情
|
||||||
|
### 通话阶段
|
||||||
|
流程可分为 4 个阶段:
|
||||||
|
* 步骤 1-5: 通话发起
|
||||||
|
* 步骤 6-7: 通话接听
|
||||||
|
* 步骤 8-15: 元数据交换
|
||||||
|
* 步骤 16-17: 通话终止
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant A as Alice
|
||||||
|
participant S as Tinode 服务器
|
||||||
|
participant B as Bob
|
||||||
|
rect rgb(212, 242, 255)
|
||||||
|
Note over A: Alice 发起通话
|
||||||
|
A->>S: 1. {pub head:webrtc=started}
|
||||||
|
S->>A: 2. {ctrl params:seq=123}
|
||||||
|
S->>+B: 3. {info seq=123 event=invite}
|
||||||
|
S-->>B: 或 {data seq=123 head:webrtc=started} <br/> 推送通知
|
||||||
|
B->>-S: 4. {note seq=123 event=ringing}
|
||||||
|
S->>A: 5. {info seq=123 event=ringing}
|
||||||
|
end
|
||||||
|
Note over S: Bob 的客户端响铃<br/>等待 Bob 接听
|
||||||
|
rect rgb(212, 242, 255)
|
||||||
|
Note over B: Bob 接听通话
|
||||||
|
B->>S: 6. {note seq=123 event=accept}
|
||||||
|
S->>A: 7a. {info seq=123 event=accept}
|
||||||
|
S->>B: 7b. {info seq=123 event=accept}
|
||||||
|
S-->>B: {data seq=124 head:webrtc=accepted,replace=123}
|
||||||
|
S-->>A: {data seq=124 head:webrtc=accepted,replace=123}
|
||||||
|
end
|
||||||
|
Note over S: 通话已接听,端元数据交换
|
||||||
|
A->>S: 8. {note seq=123 event=offer}
|
||||||
|
S->>+B: 9. {info seq=123 event=offer}
|
||||||
|
B->>-S: 10. {note seq=123 event=answer}
|
||||||
|
S->>A: 11. {info seq=123 event=answer}
|
||||||
|
rect rgb(212, 242, 255)
|
||||||
|
Note over S: ICE 候选交换
|
||||||
|
loop
|
||||||
|
A->>S: 12. {note seq=123 event=ice-candidate}
|
||||||
|
S->>B: 13. {info seq=123 event=ice-candidate}
|
||||||
|
B->>S: 14. {note seq=123 event=ice-candidate}
|
||||||
|
S->>A: 15. {info seq=123 event=ice-candidate}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Note over S: 通话已建立<br/>对话进行中
|
||||||
|
rect rgb(212, 242, 255)
|
||||||
|
Note over S: 通话终止
|
||||||
|
alt
|
||||||
|
A->>S: 16a. {note seq=123 event=hang-up}
|
||||||
|
B->>S: 16b. {note seq=123 event=hang-up}
|
||||||
|
end
|
||||||
|
alt
|
||||||
|
S->>B: 17a. {info seq=123 event=hang-up}
|
||||||
|
S->>A: 17b. {info seq=123 event=hang-up}
|
||||||
|
end
|
||||||
|
S-->>B: {data seq=125 head:webrtc=finished,replace=123}
|
||||||
|
S-->>A: {data seq=125 head:webrtc=finished,replace=123}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### 通话建立与终止步骤
|
||||||
|
|
||||||
|
#### 通话发起
|
||||||
|
1. `Alice` 通过发布视频通话消息(带 `webrtc=started` 头)发起通话
|
||||||
|
2. 服务器回复 `{ctrl}` 消息,包含通话的 `seq` id。
|
||||||
|
3. 服务器将 `invite` 事件消息路由到 `Bob`(所有客户端)。
|
||||||
|
- 此外,服务器向 `Bob` 发送包含 `webrtc=started` 字段的数据推送通知。
|
||||||
|
- 收到以上任一消息后,`Bob` 显示来电通话 UI。
|
||||||
|
4. `Bob` 回复 `ringing` 事件。
|
||||||
|
5. 服务器将 `ringing` 事件转发给 `Alice`。后者现在播放振铃音。
|
||||||
|
- 注意 `Alice` 可能收到多个 `ringing` 事件,因为 `Bob` 的每个实例分别确认收到通话邀请。
|
||||||
|
- `Alice` 和服务器将等待服务器配置的超时时间,等待 `Bob` 接听然后挂断。
|
||||||
|
- 此时,通话正式**已发起**。
|
||||||
|
|
||||||
|
#### 通话接听
|
||||||
|
6. `Bob` 通过发送 `accept` 事件接听通话。
|
||||||
|
7. (a) 和 (b): 服务器将 `accept` 事件路由到 `Alice` 和 `Bob`。
|
||||||
|
- 此外,服务器广播通话数据消息的替换消息,带 `webrtc=accepted` 头。
|
||||||
|
- 替换消息的推送通知也会发送。
|
||||||
|
- `Bob` 的会话(除接听通话的那个)可以静默关闭来电通话 UI。
|
||||||
|
- 此时,通话正式**已接听**。
|
||||||
|
|
||||||
|
#### 元数据交换
|
||||||
|
8. `Alice` 发送包含 SDP 载荷的 `offer` 事件。
|
||||||
|
9. 服务器将 `offer` 路由给 `Bob`。
|
||||||
|
10. 收到 `offer` 后,`Bob` 回复包含 SDP 载荷的 `answer` 事件。
|
||||||
|
11. 服务器将 `Bob` 的 `answer` 事件转发给 `Alice`。
|
||||||
|
|
||||||
|
步骤 12-15 是 `Alice` 和 `Bob` 之间的 Ice 候选交换。
|
||||||
|
此时通话正式**已建立**。`Alice` 和 `Bob` 可以互相看到和听到。
|
||||||
|
|
||||||
|
#### 通话终止
|
||||||
|
16. `Alice` 向服务器发送 `hang-up` 事件。
|
||||||
|
17. 服务器将 `hang-up` 事件路由给 `Bob`。
|
||||||
|
|
||||||
|
此外,服务器广播通话数据消息的替换消息,带 `webrtc=finished` 头。
|
||||||
|
替换消息的推送通知也会发送。
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
# Video Call Establishment Flow
|
||||||
|
|
||||||
|
Tinode supports peer to peer video calls over [WebRTC](https://webrtc.org/). The diagram below illustrates a call establishment flow between two users `Alice` and `Bob`. The flow is conceptually similar to [SIP](https://en.wikipedia.org/wiki/Session_Initiation_Protocol), but uses native Tinode messages for transport.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- All communication is proxied by the Tinode server.
|
||||||
|
- Client-to-server events are dispatched in `{note}` messages with the call's `topic` and `seq` fields set.
|
||||||
|
- Server-to-client data are routed in `{info}` messages on `me` topic with the call's `src` (call topic) and `seq` fields set (and/or in data push notifications).
|
||||||
|
- It's assumed that both Alice and Bob may have multiple devices.
|
||||||
|
|
||||||
|
## Details
|
||||||
|
### Call phases
|
||||||
|
The flow may be broken down into 4 phases:
|
||||||
|
* Steps 1-5: call initiation
|
||||||
|
* Steps 6-7: call acceptance
|
||||||
|
* Steps 8-15: metadata exchange
|
||||||
|
* Steps 16-17: call termination
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant A as Alice
|
||||||
|
participant S as Tinode Server
|
||||||
|
participant B as Bob
|
||||||
|
rect rgb(212, 242, 255)
|
||||||
|
Note over A: Alice initiates a call
|
||||||
|
A->>S: 1. {pub head:webrtc=started}
|
||||||
|
S->>A: 2. {ctrl params:seq=123}
|
||||||
|
S->>+B: 3. {info seq=123 event=invite}
|
||||||
|
S-->>B: or {data seq=123 head:webrtc=started} <br/> push notification
|
||||||
|
B->>-S: 4. {note seq=123 event=ringing}
|
||||||
|
S->>A: 5. {info seq=123 event=ringing}
|
||||||
|
end
|
||||||
|
Note over S: Bob's client ringing<br/>waiting for Bob to accept
|
||||||
|
rect rgb(212, 242, 255)
|
||||||
|
Note over B: Bob accepts the call
|
||||||
|
B->>S: 6. {note seq=123 event=accept}
|
||||||
|
S->>A: 7a. {info seq=123 event=accept}
|
||||||
|
S->>B: 7b. {info seq=123 event=accept}
|
||||||
|
S-->>B: {data seq=124 head:webrtc=accepted,replace=123}
|
||||||
|
S-->>A: {data seq=124 head:webrtc=accepted,replace=123}
|
||||||
|
end
|
||||||
|
Note over S: Call accepted, peer metadata exchange
|
||||||
|
A->>S: 8. {note seq=123 event=offer}
|
||||||
|
S->>+B: 9. {info seq=123 event=offer}
|
||||||
|
B->>-S: 10. {note seq=123 event=answer}
|
||||||
|
S->>A: 11. {info seq=123 event=answer}
|
||||||
|
rect rgb(212, 242, 255)
|
||||||
|
Note over S: ICE candidate exchange
|
||||||
|
loop
|
||||||
|
A->>S: 12. {note seq=123 event=ice-candidate}
|
||||||
|
S->>B: 13. {info seq=123 event=ice-candidate}
|
||||||
|
B->>S: 14. {note seq=123 event=ice-candidate}
|
||||||
|
S->>A: 15. {info seq=123 event=ice-candidate}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Note over S: Call established<br/>conversation in progress
|
||||||
|
rect rgb(212, 242, 255)
|
||||||
|
Note over S: Call termination
|
||||||
|
alt
|
||||||
|
A->>S: 16a. {note seq=123 event=hang-up}
|
||||||
|
B->>S: 16b. {note seq=123 event=hang-up}
|
||||||
|
end
|
||||||
|
alt
|
||||||
|
S->>B: 17a. {info seq=123 event=hang-up}
|
||||||
|
S->>A: 17b. {info seq=123 event=hang-up}
|
||||||
|
end
|
||||||
|
S-->>B: {data seq=125 head:webrtc=finished,replace=123}
|
||||||
|
S-->>A: {data seq=125 head:webrtc=finished,replace=123}
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
### Call Establishment & Termination steps
|
||||||
|
|
||||||
|
#### Call initiation
|
||||||
|
1. `Alice` initiates a call by posting a video call message (with `webrtc=started` header)
|
||||||
|
2. Server replies with a `{ctrl}` message containing the `seq` id of the call.
|
||||||
|
3. Server routes an `invite` event message to `Bob` (all clients).
|
||||||
|
- Additionally, server sends data push notifications containing a `webrtc=started` field to `Bob`.
|
||||||
|
- Upon receiving either of the above, `Bob` displays the incoming call UI.
|
||||||
|
4. `Bob` replies with a `ringing` event.
|
||||||
|
5. Server relays the `ringing` event to `Alice`. The latter now plays the ringing sound.
|
||||||
|
- Note that `Alice` may receive multiple `ringing` events as each separate instance of `Bob` acknowldges receipt of the call invitation separately.
|
||||||
|
- `Alice` and server will wait for up to a server configured timeout for `Bob` to accept the call and then hang up.
|
||||||
|
- At this point, the call is officially **initiated**.
|
||||||
|
|
||||||
|
#### Call acceptance
|
||||||
|
6. `Bob` accepts the call by sending an `accept` event.
|
||||||
|
7. (a) and (b): Server routes `accept` event to `Alice` and `Bob`.
|
||||||
|
- Additionally, the server broadcasts a replacement for the call data message with `webrtc=accepted` header.
|
||||||
|
- Push notifications for the replacement message are sent as well.
|
||||||
|
- `Bob`'s sessions except the one that accepted the call may silently dismiss the incoming call UI.
|
||||||
|
- At this point, the call is officially **accepted**.
|
||||||
|
|
||||||
|
#### Metadata exchange
|
||||||
|
8. `Alice` sends an `offer` event containing an SDP payload.
|
||||||
|
9. Server routes the `offer` to `Bob`.
|
||||||
|
10. Upon receiving the `offer`, `Bob` replies with an `answer` event containing an SDP payload.
|
||||||
|
11. Server forwards `Bob`'s `answer` event to `Alice`.
|
||||||
|
|
||||||
|
Steps 12-15 are Ice candidate exchange between `Alice` and `Bob`.
|
||||||
|
At this point the call is officially **established**. `Alice` and `Bob` can see and hear each other.
|
||||||
|
|
||||||
|
#### Call termination
|
||||||
|
16. `Alice` sends a `hang-up` event to server.
|
||||||
|
17. Server routes a `hang-up` event to `Bob`.
|
||||||
|
|
||||||
|
Additionally, the server broadcasts a replacement for the call data message with `webrtc=finished` header.
|
||||||
|
Push notifications for the replacement message are sent as well.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,364 @@
|
|||||||
|
# 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~~<br/>
|
||||||
|
> combined **bold and _italic_**<br/>
|
||||||
|
> an url: https://www.example.com/abc#fragment and another _[https://web.tinode.co](https://web.tinode.co)_<br/>
|
||||||
|
> this is a [@mention](#) and a [#hashtag](#) in a string<br/>
|
||||||
|
> second [#hashtag](#)<br/>
|
||||||
|
|
||||||
|
上述文本的 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`: 表单,有序字段集
|
||||||
|
|
||||||
|
表单提供了为逻辑元素组添加段落级格式的方法。它可以表示为文本装饰或实体。
|
||||||
|
<table>
|
||||||
|
<tr><th>Do you agree?</th></tr>
|
||||||
|
<tr><td><a href="">Yes</a></td></tr>
|
||||||
|
<tr><td><a href="">No</a></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
```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:`<name>=<val>`、`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: <EX 实体引用>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
_重要安全考虑_:`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` 字段内容。
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
# Drafty: Rich Message Format
|
||||||
|
|
||||||
|
Drafty is a text format used by Tinode to style messages. The intent of Drafty is to be expressive just enough without opening too many possibilities for security issues. One may think of it as JSON-encapsulated [markdown](https://en.wikipedia.org/wiki/Markdown). Drafty is influenced by FB's [draft.js](https://draftjs.org/) specification. As of the time of this writing [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) and [Swift](https://github.com/tinode/ios/blob/master/TinodeSDK/model/Drafty.swift) implementations exist. A [Go implementation](https://github.com/tinode/chat/blob/master/server/drafty/drafty.go) can convert Drafy to plain text and previews.
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
> this is **bold**, `code` and _italic_, ~~strike~~<br/>
|
||||||
|
> combined **bold and _italic_**<br/>
|
||||||
|
> an url: https://www.example.com/abc#fragment and another _[https://web.tinode.co](https://web.tinode.co)_<br/>
|
||||||
|
> this is a [@mention](#) and a [#hashtag](#) in a string<br/>
|
||||||
|
> second [#hashtag](#)<br/>
|
||||||
|
|
||||||
|
Sample Drafty-JSON representation of the text above:
|
||||||
|
```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" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
Drafty object has three fields: plain text `txt`, inline markup `fmt`, and entities `ent`.
|
||||||
|
|
||||||
|
### Plain Text
|
||||||
|
|
||||||
|
The message to be sent is converted to plain Unicode text with all markup stripped and kept in `txt` field. In general, a valid Drafy may contain just the `txt` field.
|
||||||
|
|
||||||
|
### Inline Formatting `fmt`
|
||||||
|
|
||||||
|
Inline formatting is an array of individual styles in the `fmt` field. Each style is represented by an object with at least `at` and `len` fields. The `at` value means 0-based offset into `txt`, `len` is the number of characters to apply formatting to. The third value of style is either `tp` or `key`.
|
||||||
|
|
||||||
|
If `tp` is provided, it means the style is a basic text decoration:
|
||||||
|
* `BR`: line break.
|
||||||
|
* `CO`: code or monotyped text, possibly with different background: `monotype`.
|
||||||
|
* `DL`: deleted or strikethrough text: ~~strikethrough~~.
|
||||||
|
* `EM`: emphasized text, usually represented as italic: _italic_.
|
||||||
|
* `FM`: form / set of fields; may also be represented as an entity.
|
||||||
|
* `HD`: hidden content.
|
||||||
|
* `HL`: highlighted text, such as text in a different color or with a different background; the color cannot be specified.
|
||||||
|
* `RW`: logical grouping of formats, a row; may also be represented as an entity.
|
||||||
|
* `ST`: strong or bold text: **bold**.
|
||||||
|
|
||||||
|
If key is provided, it's a 0-based index into the `ent` array which contains extended style parameters such as an image or an URL:
|
||||||
|
* `AU`: embedded audio.
|
||||||
|
* `BN`: interactive button.
|
||||||
|
* `EX`: generic attachment.
|
||||||
|
* `FM`: form / set of fields; may also be represented as a basic decoration.
|
||||||
|
* `HT`: hashtag, e.g. [#hashtag](#).
|
||||||
|
* `IM`: inline image.
|
||||||
|
* `LN`: link (URL) [https://api.tinode.co](https://api.tinode.co).
|
||||||
|
* `MN`: mention such as [@tinode](#).
|
||||||
|
* `RW`: logical grouping of formats, a row; may also be represented as a basic decoration.
|
||||||
|
* `VC`: video (and audio) calls.
|
||||||
|
* `VD`: inline video.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
* `{ "at":8, "len":4, "tp":"ST"}`: apply formatting `ST` (strong/bold) to 4 characters starting at offset 8 into `txt`.
|
||||||
|
* `{ "at":144, "len":8, "key":2 }`: insert entity `ent[2]` into position 144, the entity spans 8 characters.
|
||||||
|
* `{ "at":-1, "len":0, "key":4 }`: show the `ent[4]` as a file attachment, don't apply any styling to text.
|
||||||
|
|
||||||
|
The clients should be able to handle missing `at`, `key`, and `len` values. Missing values are assumed to be equal to `0`.
|
||||||
|
|
||||||
|
The indexes `at` and `len` are measured in [Unicode code points](https://developer.mozilla.org/en-US/docs/Glossary/Code_point), not bytes or characters. The behavior of multi-codepoint glyphs such as emojis with Fitzpatrick skin tone modifiers, variation selectors, or grouped with `ZWJ` is currently undefined.
|
||||||
|
|
||||||
|
#### `FM`: a form, an ordered set or fields
|
||||||
|
|
||||||
|
Form provides means to add paragraph-level formatting to a logical group of elements. It may be represented as a text decoration or as an entity.
|
||||||
|
<table>
|
||||||
|
<tr><th>Do you agree?</th></tr>
|
||||||
|
<tr><td><a href="">Yes</a></td></tr>
|
||||||
|
<tr><td><a href="">No</a></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"txt": "Do you agree? Yes No",
|
||||||
|
"fmt": [
|
||||||
|
{"len": 20, "tp": "FM"}, // missing 'at' is zero: "at": 0
|
||||||
|
{"len": 13, "tp": "ST"}
|
||||||
|
{"at": 13, "len": 1, "tp": "BR"},
|
||||||
|
{"at": 14, "len": 3}, // missing 'key' is zero: "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"}}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
If a `Yes` button is pressed in the example above, the client is expected to send a message to the server with the following content:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"txt": "Yes",
|
||||||
|
"fmt": [{
|
||||||
|
"at":-1
|
||||||
|
}],
|
||||||
|
"ent": [{
|
||||||
|
"tp": "EX",
|
||||||
|
"data": {
|
||||||
|
"mime": "text/x-drafty-fr", // drafty form-response.
|
||||||
|
"val": {
|
||||||
|
"seq": 15, // seq id of the message containing the form.
|
||||||
|
"resp": {"yes": "oh yes!"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The form may be optionally represented as an entity:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"tp": "FM",
|
||||||
|
"data": {
|
||||||
|
"su": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
The `data.su` describes how interactive form elements behave after the click. An `"su": true` indicates that the form is `single use`: the form should change after the first interaction to show that it's no longer accepting input.
|
||||||
|
|
||||||
|
### Entities `ent`
|
||||||
|
|
||||||
|
In general, an entity is a text decoration which requires additional (possibly large) data. An entity is represented by an object with two fields: `tp` indicates type of the entity, `data` is type-dependent styling information. Unknown fields are ignored.
|
||||||
|
|
||||||
|
#### `AU`: embedded audio record
|
||||||
|
`AU` is an audio record. The `data` contains the following fields:
|
||||||
|
```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`: data type, such as 'audio/ogg'.
|
||||||
|
* `val`: optional in-band audio data: base64-encoded audio bits.
|
||||||
|
* `ref`: optional reference to out-of-band audio data. Either `val` or `ref` must be present.
|
||||||
|
* `preview`: base64-encoded array of bytes to generate a visual preview; each byte is an amplitude bar.
|
||||||
|
* `duration`: duration of the record in milliseconds.
|
||||||
|
* `name`: optional name of the original file.
|
||||||
|
* `size`: optional size of the file in bytes.
|
||||||
|
|
||||||
|
To create a message with just a single audio record and no text, use the following Drafty:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
txt: " ",
|
||||||
|
fmt: [{len: 1}],
|
||||||
|
ent: [{tp: "AU", data: {<your audio data here>}]}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
_IMPORTANT Security Consideration_: the `val` and `ref` fields may contain malicious payload. The client should restrict URL scheme in the `ref` field to `http` and `https` only. The client should present content of `val` field to the user only if it's correctly converted to an audio.
|
||||||
|
|
||||||
|
|
||||||
|
#### `BN`: interactive button
|
||||||
|
`BN` offers an option to send data to a server, either origin server or another one. The `data` contains the following fields:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"tp": "BN",
|
||||||
|
"data": {
|
||||||
|
"name": "confirmation",
|
||||||
|
"act": "url",
|
||||||
|
"val": "some-value",
|
||||||
|
"ref": "https://www.example.com/path/?foo=bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* `act`: type of action in response to button click:
|
||||||
|
* `pub`: send a Drafty-formatted `{pub}` message to the current topic with the form data as an attachment:
|
||||||
|
```js
|
||||||
|
{ "tp":"EX", "data":{ "mime":"text/x-drafty-fr", "val": { "seq": 3, "resp": { "confirmation": "some-value" } } } }
|
||||||
|
```
|
||||||
|
* `url`: issue an `HTTP GET` request to the URL from the `data.ref` field. The following query parameters are appended to the URL: `<name>=<val>`, `uid=<current-user-ID>`, `topic=<topic name>`, `seq=<message sequence ID>`.
|
||||||
|
* `note`: send a `{note}` message to the current topic with `what` set to `data` (not currently implemented, contact us if you need it).
|
||||||
|
```js
|
||||||
|
{ "what": "data", "data": { "mime": "text/x-drafty-fr", "val": { "seq": 3, "resp": { "confirmation": "some-value" } } }
|
||||||
|
```
|
||||||
|
* `name`: optional name of the button which is reported back to the server.
|
||||||
|
* `val`: additional opaque data.
|
||||||
|
* `ref`: the URL for the `url` action.
|
||||||
|
|
||||||
|
If the `name` is provided but `val` is not, `val` is assumed to be `1`. If `name` is undefined then nether `name` nor `val` are sent.
|
||||||
|
|
||||||
|
The button in the example above will send an HTTP GET to https://www.example.com/path/?foo=bar&confirmation=some-value&uid=usrFsk73jYRR&topic=grpnG99YhENiQU&seq=3 assuming the current user ID is `usrFsk73jYRR`, the topic is `grpnG99YhENiQU`, and the sequence ID of message with the button is `3`.
|
||||||
|
|
||||||
|
_IMPORTANT Security Consideration_: the client should restrict URL scheme in the `ref` field to `http` and `https` only.
|
||||||
|
|
||||||
|
|
||||||
|
#### `EX`: file attachment
|
||||||
|
`EX` is an attachment which the client should not try to interpret. The `data` contains the following fields:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"tp": "EX",
|
||||||
|
"data": {
|
||||||
|
"mime", "text/plain",
|
||||||
|
"val", "Q3l0aG9uPT0w...PT00LjAuMAo=",
|
||||||
|
"ref": "/v0/file/s/abcdef12345.txt",
|
||||||
|
"name", "requirements.txt",
|
||||||
|
"size": 1234
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
* `mime`: data type, such as 'application/octet-stream'.
|
||||||
|
* `val`: optional in-band base64-encoded file data.
|
||||||
|
* `ref`: optional reference to out-of-band file data. Either `val` or `ref` must be present.
|
||||||
|
* `name`: optional name of the original file.
|
||||||
|
* `size`: optional size of the file in bytes.
|
||||||
|
|
||||||
|
To generate a message with the file attachment shown as a downloadable file, use the following format:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
at: -1,
|
||||||
|
len: 0,
|
||||||
|
key: <EX entity reference>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
_IMPORTANT Security Consideration_: the `ref` fields may contain malicious payload. The client should restrict URL scheme in the `ref` field to `http` and `https` only.
|
||||||
|
|
||||||
|
|
||||||
|
#### `IM`: inline image or attached image with inline preview
|
||||||
|
`IM` is an image. The `data` contains the following fields:
|
||||||
|
```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`: data type, such as 'image/jpeg'.
|
||||||
|
* `val`: optional in-band image data: base64-encoded image bits.
|
||||||
|
* `ref`: optional reference to out-of-band image data. Either `val` or `ref` must be present.
|
||||||
|
* `width`, `height`: linear dimensions of the image in pixels.
|
||||||
|
* `name`: optional name of the original file.
|
||||||
|
* `size`: optional size of the file in bytes.
|
||||||
|
|
||||||
|
To create a message with just a single image and no text, use the following Drafty:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
txt: " ",
|
||||||
|
fmt: [{len: 1}],
|
||||||
|
ent: [{tp: "IM", data: {<your image data here>}]}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
_IMPORTANT Security Consideration_: the `val` and `ref` fields may contain malicious payload. The client should restrict URL scheme in the `ref` field to `http` and `https` only. The client should present content of `val` field to the user only if it's correctly converted to an image.
|
||||||
|
|
||||||
|
|
||||||
|
#### `LN`: link (URL)
|
||||||
|
`LN` is an URL. The `data` contains a single `url` field:
|
||||||
|
`{ "tp": "LN", "data": { "url": "https://www.example.com/abc#fragment" } }`
|
||||||
|
The `url` could be any valid URL that the client knows how to interpret, for instance it could be email or phone URL too: `email:alice@example.com` or `tel:+17025550001`.
|
||||||
|
|
||||||
|
_IMPORTANT Security Consideration_: the `url` field may be maliciously constructed. The client should disable certain URL schemes such as `javascript:` and `data:`.
|
||||||
|
|
||||||
|
|
||||||
|
#### `MN`: mention such as [@alice](#)
|
||||||
|
Mention `data` contains a single `val` field with ID of the mentioned user:
|
||||||
|
```js
|
||||||
|
{ "tp":"MN", "data":{ "val":"usrFsk73jYRR" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### `HT`: hashtag, e.g. [#tinode](#)
|
||||||
|
Hashtag `data` contains a single `val` field with the hashtag value which the client software needs to interpret, for instance it could be a search term:
|
||||||
|
```js
|
||||||
|
{ "tp":"HT", "data":{ "val":"tinode" } }
|
||||||
|
```
|
||||||
|
|
||||||
|
#### `VC`: video call control message
|
||||||
|
Video call `data` contains current state of the call and its duration:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
"tp": "VC",
|
||||||
|
"data": {
|
||||||
|
"duration": 10000,
|
||||||
|
"state": "disconnected",
|
||||||
|
"incoming": false,
|
||||||
|
"aonly": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
* `duration`: call duration in milliseconds.
|
||||||
|
* `state`: surrent call state; supported states:
|
||||||
|
* `accepted`: a call is established (ongoing).
|
||||||
|
* `busy`: a call cannot be established because the callee is already in another call.
|
||||||
|
* `finished`: a previously establied call has successfully finished.
|
||||||
|
* `disconnected`: the call is dropped, for example because of an error.
|
||||||
|
* `missed`: the call is missed, i.e. the callee didn't pick up the phone.
|
||||||
|
* `declined`: the call is declined, i.e. the callee hung up before picking up.
|
||||||
|
* `incoming`: true if the call is incoming, otherwise the call is outgoing.
|
||||||
|
* `aonly`: true if this is an audio-only call (no video).
|
||||||
|
|
||||||
|
The `VC` may also be represented as a format `"fmt": [{"len": 1, "tp": "VC"}]` with no entity. In such a case all call information is contained in the `head` fields of the enclosing message.
|
||||||
|
|
||||||
|
#### `VD`: video with preview
|
||||||
|
`VD` represents a video recording. The `data` contains the following fields:
|
||||||
|
```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`: data type of the video, such as 'video/webm'.
|
||||||
|
* `val`: optional in-band video data: base64-encoded video bits, usually not present (null).
|
||||||
|
* `ref`: optional reference to an out-of-band video data. Either `val` or `ref` must be present.
|
||||||
|
* `preview`: optional base64-encoded screencapture image from the video (poster).
|
||||||
|
* `preref`: optional reference to an out-of-band screencapture image from the video (poster).
|
||||||
|
* `premime`: data type of the optional screencapture image (poster); assumed 'image/jpeg' if missing.
|
||||||
|
* `width`, `height`: linear dimensions of the video and poster in pixels.
|
||||||
|
* `duration`: duration of the video in milliseconds.
|
||||||
|
* `name`: optional name of the original file.
|
||||||
|
* `size`: optional size of the file in bytes.
|
||||||
|
|
||||||
|
To create a message with just a single video and no text, use the following Drafty:
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
txt: " ",
|
||||||
|
fmt: [{len: 1}],
|
||||||
|
ent: [{tp: "VD", data: {<your video data here>}]}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
_IMPORTANT Security Consideration_: the `val`, `ref`, `preview` fields may contain malicious payload. The client should restrict URL scheme in the `ref` and `preview` fields to `http` and `https` only. The client should present content of `val` field to the user only if it's correctly converted to a video.
|
||||||
|
|||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# 常见问题解答
|
||||||
|
|
||||||
|
### Q: 在 Docker 中运行时,服务器日志在哪里?<br/>
|
||||||
|
**A**: 日志在容器内的 `/var/log/tinode.log`。使用以下命令附加到运行中的容器:
|
||||||
|
```
|
||||||
|
docker exec -it name-of-the-running-container /bin/bash
|
||||||
|
```
|
||||||
|
然后,例如用 `tail -50 /var/log/tinode.log` 查看日志。
|
||||||
|
|
||||||
|
如果容器已停止,可以从容器复制日志(保存到 `./tinode.log`):
|
||||||
|
```
|
||||||
|
docker cp name-of-the-container:/var/log/tinode.log ./tinode.log
|
||||||
|
```
|
||||||
|
|
||||||
|
或者,可以通过将主机目录映射到容器内的 `/var/log/`,让 Docker 容器将日志保存到主机目录。在 `docker run` 命令中添加 `-v /where/to/save/logs:/var/log`。
|
||||||
|
|
||||||
|
|
||||||
|
### Q: 启用推送通知有哪些选项?<br/>
|
||||||
|
**A**: 可以使用 [Tinode Push Gateway (TNPG)](https://github.com/tinode/chat/tree/master/server/push/tnpg) 或 [Google FCM](https://firebase.google.com/docs/cloud-messaging):
|
||||||
|
* _Tinode Push Gateway_ 使用 Tinode 服务器代您发送推送。需要最少的设置:您的服务器向 TNPG 发送请求,TNPG 转发给 Google FCM 或 Apple APNS。
|
||||||
|
* _Google FCM_ 不依赖 Tinode 基础设施推送,但需要您构建和发布自己的移动应用(iOS 和 Android)。
|
||||||
|
|
||||||
|
|
||||||
|
### Q: 如何使用 Tinode Push Gateway 设置推送通知?<br/>
|
||||||
|
**A**: 启用 TNPG 推送通知需要两步:
|
||||||
|
* 在 [console.tinode.co](https://console.tinode.co) 注册并获取 TNPG 令牌。
|
||||||
|
* 使用令牌配置服务器。
|
||||||
|
详见[此处](../server/push/tnpg/)。
|
||||||
|
|
||||||
|
|
||||||
|
### Q: 如何使用 Google FCM 设置推送通知?<br/>
|
||||||
|
**A**: 此选项需要您构建和发布自己的移动应用。如果您不想这样做,请使用上面的 TNPG 选项。
|
||||||
|
|
||||||
|
启用 FCM 推送通知需要以下步骤:
|
||||||
|
* 启用服务器端推送发送。
|
||||||
|
* 启用客户端推送接收。
|
||||||
|
|
||||||
|
#### 服务器和 TinodeWeb
|
||||||
|
|
||||||
|
1. 如尚未创建,请在 https://firebase.google.com/ 创建项目。
|
||||||
|
2. 按照 https://cloud.google.com/iam/docs/creating-managing-service-account-keys 的说明下载凭据文件。
|
||||||
|
3. 更新服务器配置 [`tinode.conf`](../server/tinode.conf#L255),在 `"push"` -> `"name": "fcm"` 部分。执行以下**任一**操作:
|
||||||
|
* 将下载的凭据文件路径填入 `"credentials_file"`。
|
||||||
|
* 或将文件内容复制到 `"credentials"`。<br/><br/>
|
||||||
|
删除另一个条目。例如,如果更新了 `"credentials_file"`,则删除 `"credentials"`,反之亦然。
|
||||||
|
4. 更新 [TinodeWeb](/tinode/webapp/) 配置 [`firebase-init.js`](https://github.com/tinode/webapp/blob/master/firebase-init.js):更新 `apiKey`、`messagingSenderId`、`projectId`、`appId`、`messagingVapidKey`。详见 https://github.com/tinode/webapp/#push_notifications
|
||||||
|
|
||||||
|
#### iOS 和 Android
|
||||||
|
1. 如果使用 Android 客户端,按照 https://developers.google.com/android/guides/google-services-plugin 的说明将 `google-services.json` 添加到 [Tindroid](/tinode/tindroid/) 并重新编译客户端。您也可以选择提交到 Google Play Store。
|
||||||
|
详见 https://github.com/tinode/tindroid/#push_notifications
|
||||||
|
2. 如果使用 iOS 客户端,按照 https://firebase.google.com/docs/cloud-messaging/ios/client 的说明将 `GoogleService-Info.plist` 添加到 [Tinodios](/tinode/ios/) 并重新编译客户端。您也可以选择提交到 Apple AppStore。
|
||||||
|
详见 https://github.com/tinode/ios/#push_notifications
|
||||||
|
|
||||||
|
|
||||||
|
### Q: 如何添加新用户?<br/>
|
||||||
|
**A**: 创建账户有三种方式:
|
||||||
|
* 用户可以使用应用程序(Web、Android、iOS)创建新账户。
|
||||||
|
* 可以使用 [tn-cli](../tn-cli/) 创建新账户(`acc` 命令或 `useradd` 宏)。该过程可脚本化。
|
||||||
|
* 如果用户已存在于外部数据库,可以在首次登录时使用 [rest authenticator](../server/auth/rest/) 自动创建 Tinode 账户。
|
||||||
|
|
||||||
|
|
||||||
|
### Q: 如何让我的安装私有化?<br/>
|
||||||
|
**A**: 如果想限制只有您批准的人才能注册,最简单的方法是将 Tinode 注册限制到您控制的邮件域名:注册自定义域名,在域名注册商处设置 catch-all 邮件转发服务(通常免费)。然后在 Tinode 配置中使用您的域名(`"acc_validation" -> "email" -> "domains"`,例如 `"domains": ["my-domain.com"]`)。您会在 catch-all 邮箱收到注册邮件,然后可以手动转发验证码给您的用户。或者,如果用户很多,可以使用 [rest authenticator](../server/auth/rest/)。
|
||||||
|
|
||||||
|
|
||||||
|
### Q: 如何创建 `root` 用户?<br/>
|
||||||
|
**A**: 从 Tinode 0.18 版本开始,可以通过运行以下命令授予用户 `root` 权限:
|
||||||
|
```sh
|
||||||
|
./tinode-db -auth=ROOT -uid=usrAbcDef123 -scheme=basic
|
||||||
|
```
|
||||||
|
从 0.21 版本开始,可以使用更简单的命令:
|
||||||
|
```sh
|
||||||
|
./tinode-db -make_root=usrAbcDef123
|
||||||
|
```
|
||||||
|
其中 `usrAbcDef123` 是要更新的用户 ID。
|
||||||
|
|
||||||
|
在 0.17 及更早版本中,只能通过执行数据库查询授予用户 `root` 权限。
|
||||||
|
首先创建或选择要提升为 `root` 的用户,然后执行查询:
|
||||||
|
* RethinkDB:
|
||||||
|
```js
|
||||||
|
r.db('tinode').table('auth').get('basic:login-of-the-user-to-make-root').update({authLvl: 30})
|
||||||
|
```
|
||||||
|
* MySQL, PostgreSQL:
|
||||||
|
```sql
|
||||||
|
USE 'tinode';
|
||||||
|
UPDATE auth SET authlvl=30 WHERE uname='basic:login-of-the-user-to-make-root';
|
||||||
|
```
|
||||||
|
* MongoDB:
|
||||||
|
```js
|
||||||
|
db.getCollection('auth').updateOne({_id: 'basic:login-of-the-user-to-make-root'}, {$set: {authlvl: 30}})
|
||||||
|
```
|
||||||
|
测试数据库有一个预设的 `xena` 用户具有 root 权限。
|
||||||
|
|
||||||
|
|
||||||
|
### Q: 当网络连接数达到每节点约 1000 时,各种问题开始出现。这是 bug 吗?<br/>
|
||||||
|
**A**: 这很可能不是 bug。为确保服务器良好性能,Linux 在内核级别限制每个进程的打开文件描述符(活动网络连接、打开文件)总数。默认限制通常是 1024。文件描述符数量还有其他可能的限制。您遇到的问题很可能是因为超过了某个 Linux 限制。请寻求系统管理员的帮助。
|
||||||
|
|
||||||
|
|
||||||
|
### Q: 群组话题和频道有什么区别?<br/>
|
||||||
|
**A**: 频道是群组话题的特例。普通群组话题允许有限数量的订阅者(默认 128)。每个订阅者可以单独管理:邀请、移除、封禁、提升为管理员或所有者,其他访问权限也可以单独调整。启用频道功能的群组话题额外允许无限数量的 `读者`。读者对话题有只读访问权限,不能单独管理,不能被邀请或移除,不能发布消息。读者加入或退出话题时不生成在线状态通知,也不接收普通群组成员的在线状态通知。读者接收的频道消息的 `From` 字段为 `null`,即他们不知道谁发布了频道中的任何特定消息。读者不能删除频道消息。
|
||||||
|
|
||||||
|
|
||||||
|
### Q: 格式化 gRPC {pub content} 的正确方式是什么?<br/>
|
||||||
|
**A**: gRPC 将 `{pub}` 消息的 `content` 字段作为字节数组发送,而客户端应用期望它是有效的 JSON。因此,在传递给 gRPC 之前,必须将字段格式化为有效的 JSON。例如,要发送纯文本 `Hello world` 消息,必须发送带引号的字符串 `"Hello world"`。大多数情况下,传递给 gRPC 调用的字符串看起来像 `"\"Hello world\""` 或 `'"Hello world"'`。
|
||||||
|
|
||||||
|
|
||||||
|
### Q: 如何修复 PostgreSQL 初始化因 'missing database' 错误而失败的问题?<br/>
|
||||||
|
**A**: PostgreSQL 有一个(不当)特性:数据库连接必须始终选择一个数据库。如果连接尝试使用不存在的数据库(即使是为了创建它),连接会失败。当 Tinode 首次启动时,它尝试创建数据库,通常是 `tinode`(见 `tinode.conf`,`"store_config": {"adapters": {"postgres": {"DBName": "tinode"}}}`。数据库 `tinode` 显然不存在,所以 Tinode 连接回退到 'default' 数据库,该数据库与连接的 PostgreSQL 用户同名。默认配置指定用户为 `postgres`(`"User": "postgres"`),数据库 `postgres` 总是存在,所以连接成功,一切正常。但如果您将用户更改为 `postgres` 以外的任何值,比如 `tinodeadmin`,麻烦就开始了:名为 `tinodeadmin` 的数据库不存在,连接失败。如果要将用户名更改为 `postgres` 以外的任何值,必须创建数据库 `tinode`(或您为 Tinode 数据库命名的任何名称)或创建一个与您的用户 `tinodeadmin` 同名的空数据库。例如:
|
||||||
|
```
|
||||||
|
$ psql
|
||||||
|
postgres=# create database tinode;
|
||||||
|
exit
|
||||||
|
```
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
# Frequently Asked Questions
|
||||||
|
|
||||||
|
### Q: Where can I find server logs when running in Docker?<br/>
|
||||||
|
**A**: The log is in the container at `/var/log/tinode.log`. Attach to a running container with command
|
||||||
|
```
|
||||||
|
docker exec -it name-of-the-running-container /bin/bash
|
||||||
|
```
|
||||||
|
Then, for instance, see the log with `tail -50 /var/log/tinode.log`
|
||||||
|
|
||||||
|
If the container has stopped already, you can copy the log out of the container (saving it to `./tinode.log`):
|
||||||
|
```
|
||||||
|
docker cp name-of-the-container:/var/log/tinode.log ./tinode.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can instruct the docker container to save the logs to a directory on the host by mapping a host directory to `/var/log/` in the container. Add `-v /where/to/save/logs:/var/log` to the `docker run` command.
|
||||||
|
|
||||||
|
|
||||||
|
### Q: What are the options for enabling push notifications?<br/>
|
||||||
|
**A**: You can use [Tinode Push Gateway (TNPG)](https://github.com/tinode/chat/tree/master/server/push/tnpg) or you can use [Google FCM](https://firebase.google.com/docs/cloud-messaging):
|
||||||
|
* _Tinode Push Gateway_ uses Tinode servers to send pushes on your behalf. It requires minumum setup: your server sends request to TNPG, which forwards it to Google FCM or Apple APNS.
|
||||||
|
* _Google FCM_ does not rely on Tinode infrastructure for pushes but requires you to build and release your own mobile apps (iOS and Android).
|
||||||
|
|
||||||
|
|
||||||
|
### Q: How to setup push notifications with Tinode Push Gateway?<br/>
|
||||||
|
**A**: Enabling TNPG push notifications requires two steps:
|
||||||
|
* register at [console.tinode.co](https://console.tinode.co) and obtain a TNPG token.
|
||||||
|
* configure server with the token.
|
||||||
|
See detailed instructions [here](../server/push/tnpg/).
|
||||||
|
|
||||||
|
|
||||||
|
### Q: How to setup push notifications with Google FCM?<br/>
|
||||||
|
**A**: This option requires you to build and release your own mobile apps. If you do not want to do it, use the TNPG option above.
|
||||||
|
|
||||||
|
Enabling FCM push notifications requires the following steps:
|
||||||
|
* enable push sending from the server.
|
||||||
|
* enable receiving pushes in the clients.
|
||||||
|
|
||||||
|
#### Server and TinodeWeb
|
||||||
|
|
||||||
|
1. Create a project at https://firebase.google.com/ if you have not done so already.
|
||||||
|
2. Follow instructions at https://cloud.google.com/iam/docs/creating-managing-service-account-keys to download the credentials file.
|
||||||
|
3. Update the server config [`tinode.conf`](../server/tinode.conf#L255), section `"push"` -> `"name": "fcm"`. Do _ONE_ of the following:
|
||||||
|
* _Either_ enter the path to the downloaded credentials file into `"credentials_file"`.
|
||||||
|
* _OR_ copy the file contents to `"credentials"`.<br/><br/>
|
||||||
|
Remove the other entry. I.e. if you have updated `"credentials_file"`, remove `"credentials"` and vice versa.
|
||||||
|
4. Update [TinodeWeb](/tinode/webapp/) config [`firebase-init.js`](https://github.com/tinode/webapp/blob/master/firebase-init.js): update `apiKey`, `messagingSenderId`, `projectId`, `appId`, `messagingVapidKey`. See more info at https://github.com/tinode/webapp/#push_notifications
|
||||||
|
|
||||||
|
#### iOS and Android
|
||||||
|
1. If you are using an Android client, add `google-services.json` to [Tindroid](/tinode/tindroid/) by following instructions at https://developers.google.com/android/guides/google-services-plugin and recompile the client. You may also optionally submit it to Google Play Store.
|
||||||
|
See more info at https://github.com/tinode/tindroid/#push_notifications
|
||||||
|
2. If you are using an iOS client, add `GoogleService-Info.plist` to [Tinodios](/tinode/ios/) by following instructions at https://firebase.google.com/docs/cloud-messaging/ios/client) and recompile the client. You may optionally submit the app to Apple AppStore.
|
||||||
|
See more info at https://github.com/tinode/ios/#push_notifications
|
||||||
|
|
||||||
|
|
||||||
|
### Q: How to add new users?<br/>
|
||||||
|
**A**: There are three ways to create accounts:
|
||||||
|
* A user can create a new account using one of the applications (web, Android, iOS).
|
||||||
|
* A new account can be created using [tn-cli](../tn-cli/) (`acc` command or `useradd` macro). The process can be scripted.
|
||||||
|
* If the user already exists in an external database, the Tinode account can be automatically created on the first login using the [rest authenticator](../server/auth/rest/).
|
||||||
|
|
||||||
|
|
||||||
|
### Q: How do I make my installation private?<br/>
|
||||||
|
**A**: If you want to restrict registrations to only those people whom you approve, then the simplest way is to restrict Tinode registrations to an email domain you control: register a custom domain, set up a catch-all email forwarding service at your domain registrar (usually free). Then use your domain name in Tinode config (`"acc_validation" -> "email" -> "domains"`, for example `"domains": ["my-domain.com"]`). You will receive registration emails at your catch-all email box and you will be able to forward validation codes to your users manually. Alternatively, if you have a lot of users, you can use [rest authenticator](../server/auth/rest/).
|
||||||
|
|
||||||
|
|
||||||
|
### Q: How to create a `root` user?<br/>
|
||||||
|
**A**: Starting with Tinode version 0.18 the `root` access can be granted to a user by running the following command:
|
||||||
|
```sh
|
||||||
|
./tinode-db -auth=ROOT -uid=usrAbcDef123 -scheme=basic
|
||||||
|
```
|
||||||
|
Starting with 0.21 you can use a simpler command:
|
||||||
|
```sh
|
||||||
|
./tinode-db -make_root=usrAbcDef123
|
||||||
|
```
|
||||||
|
Where `usrAbcDef123` is the ID of the user to update.
|
||||||
|
|
||||||
|
In version 0.17 and older the `root` access can be granted to a user only by executing a database query.
|
||||||
|
First create or choose the user you want to promote to `root` then execute the query:
|
||||||
|
* RethinkDB:
|
||||||
|
```js
|
||||||
|
r.db('tinode').table('auth').get('basic:login-of-the-user-to-make-root').update({authLvl: 30})
|
||||||
|
```
|
||||||
|
* MySQL, PostgreSQL:
|
||||||
|
```sql
|
||||||
|
USE 'tinode';
|
||||||
|
UPDATE auth SET authlvl=30 WHERE uname='basic:login-of-the-user-to-make-root';
|
||||||
|
```
|
||||||
|
* MongoDB:
|
||||||
|
```js
|
||||||
|
db.getCollection('auth').updateOne({_id: 'basic:login-of-the-user-to-make-root'}, {$set: {authlvl: 30}})
|
||||||
|
```
|
||||||
|
The test database has a stock user `xena` which has root access.
|
||||||
|
|
||||||
|
|
||||||
|
### Q: Once the number of network connections reaches about 1000 per node, all kinds of problems start. Is this a bug?<br/>
|
||||||
|
**A**: It is likely not a bug. To ensure good server performance Linux limits the total number of open file descriptors (live network connections, open files) for each process at the kernel level. The default limit is usually 1024. There are other possible restrictions on the number of file descriptors. The problems you are experiencing are likely caused by exceeding one of the Linux-imposed limits. Please seek assistance of a system administrator.
|
||||||
|
|
||||||
|
|
||||||
|
### Q: What is the difference between a group topic and a channel?<br/>
|
||||||
|
**A**: Channel is a special case of a group topic. Normal group topics allow limited number of subscribers (128 by default). Each subscriber can be managed individually: invited, removed, banned, promoted to administrator or owner, other access permissions can be personally adjusted. Group topics with enabled channel functionality additionally permit an unlimited number of `readers`. The readers have read-only access to the topic, they cannot be managed individually, cannot be invited or removed, they cannot post messages. Readers do not generate presence notifications when joining or un-joining the topic and do not receive presence notifications from normal group members. Readers receive channel messages with `From` field set to `null`, i.e. they do not know who personally posted any given message to the channel. Readers cannot delete channel messages.
|
||||||
|
|
||||||
|
|
||||||
|
### Q: What is the proper way to format gRPC {pub content}?<br/>
|
||||||
|
**A**: The gPRC sends `content` field of a `{pub}` message as a byte array while the client applications expect it to be valid JSON. Consequently, you have to format the field to be valid JSON before passing it to gRPC. For example, to send a plain text `Hello world` message you have to send a quoted string `"Hello world"`. In most cases the string you pass to the gRPC call would look like `"\"Hello world\""` or `'"Hello world"'`.
|
||||||
|
|
||||||
|
|
||||||
|
### Q: How to fix PostgreSQL initialization failing with 'missing database' error?<br/>
|
||||||
|
**A**: PostgreSQL has a (mis)feature: a DB connection must always select a database. If the connection tries to use a database which does not exist (even with intent to create it), the connection fails. When Tinode is started for the first time, it tries to create a database, usually `tinode` (see `tinode.conf`, `"store_config": {"adapters": {"postgres": {"DBName": "tinode"}}}`. The database `tinode` obviously does not exist, so Tinode connection falls back to 'default' database which has the same name as the name of the connecting PostgreSQL user. The default configuration specifies user as `postgres` (`"User": "postgres"`), the database `postgres` always exists, so the connection succeeds and everything works as expected. But if you change the user to anything other than `postgres`, let's say `tinodeadmin`, then trouble starts: the database with the name `tinodeadmin` does not exist and the connection fails. If you want to change the user name to anything other than `postgres`, then you must create either a database `tinode` (or whatever you named your Tinode database) or an empty database with the same name as your user `tinodeadmin`. For example:
|
||||||
|
```
|
||||||
|
$ psql
|
||||||
|
postgres=# create database tinode;
|
||||||
|
exit
|
||||||
|
```
|
||||||
|
|||||||
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 717 B |
|
After Width: | Height: | Size: 66 KiB |
|
After Width: | Height: | Size: 84 KiB |