feat: switch client API branding to MeetingIM
This commit is contained in:
+1
-1
@@ -45,7 +45,7 @@ rm -fR ./releases/tmp
|
||||
mkdir -p ./releases/tmp/templ
|
||||
|
||||
# Copy templates and database initialization files
|
||||
cp ./server/tinode.conf ./releases/tmp
|
||||
cp ./server/meetingim.conf ./releases/tmp
|
||||
cp ./server/templ/*.templ ./releases/tmp/templ
|
||||
cp ./tinode-db/data.json ./releases/tmp
|
||||
cp ./tinode-db/*.jpg ./releases/tmp
|
||||
|
||||
@@ -82,7 +82,7 @@ type audit struct {
|
||||
func main() {
|
||||
baseURL := flag.String("base", "https://api.118im.com", "HTTP base URL")
|
||||
wsURL := flag.String("ws", "wss://api.118im.com/v0/channels", "WebSocket URL")
|
||||
apiKey := flag.String("apikey", "AQAAAAABAAAoeOI7tA3HsYvdzDhYhZJy", "Tinode API key")
|
||||
apiKey := flag.String("apikey", "AQAAAAABAAAoeOI7tA3HsYvdzDhYhZJy", "MeetingIM API key")
|
||||
tenant := flag.String("tenant", "default", "tenant code")
|
||||
out := flag.String("out", "docs/test/im_frontend_backend_1_11_testenv_results_2026-08-20.md", "report path")
|
||||
flag.Parse()
|
||||
@@ -442,7 +442,7 @@ func (a *audit) call(method, path string, body any, token string, idem bool) (in
|
||||
if err != nil {
|
||||
return 0, imResp{}, "", err
|
||||
}
|
||||
req.Header.Set("X-Tinode-APIKey", a.apiKey)
|
||||
req.Header.Set("X-MeetingIM-APIKey", a.apiKey)
|
||||
req.Header.Set("X-IM-Schema", "1")
|
||||
req.Header.Set("X-Request-ID", "audit-"+strings.ReplaceAll(time.Now().Format("150405.000000000"), ".", ""))
|
||||
req.Header.Set("X-Tenant-Code", a.tenantCode)
|
||||
@@ -452,7 +452,7 @@ func (a *audit) call(method, path string, body any, token string, idem bool) (in
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
if strings.HasPrefix(path, "/v0/file/") {
|
||||
req.Header.Set("X-Tinode-Auth", "token "+base64.StdEncoding.EncodeToString([]byte(token)))
|
||||
req.Header.Set("X-MeetingIM-Auth", "token "+base64.StdEncoding.EncodeToString([]byte(token)))
|
||||
}
|
||||
}
|
||||
if idem {
|
||||
|
||||
@@ -70,7 +70,7 @@ type wsClient struct {
|
||||
func main() {
|
||||
baseURL := flag.String("base", "https://api.118im.com", "HTTP base URL")
|
||||
wsURL := flag.String("ws", "wss://api.118im.com/v0/channels", "WebSocket URL")
|
||||
apiKey := flag.String("apikey", "AQAAAAABAAAoeOI7tA3HsYvdzDhYhZJy", "Tinode API key")
|
||||
apiKey := flag.String("apikey", "AQAAAAABAAAoeOI7tA3HsYvdzDhYhZJy", "MeetingIM API key")
|
||||
tenant := flag.String("tenant", "default", "tenant code")
|
||||
out := flag.String("out", "docs/test/im_websocket_testenv_results_2026-08-21.md", "report path")
|
||||
flag.Parse()
|
||||
@@ -601,7 +601,7 @@ func (a *audit) uploadFile() (string, error) {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||
req.Header.Set("X-Tinode-APIKey", a.apiKey)
|
||||
req.Header.Set("X-MeetingIM-APIKey", a.apiKey)
|
||||
req.Header.Set("X-Tenant-Code", a.tenantCode)
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
@@ -771,7 +771,7 @@ func (c *wsClient) pubAndPeerReceives(peer *wsClient, topic, text string) (int,
|
||||
"id": id,
|
||||
"topic": topic,
|
||||
"head": map[string]any{"mime": "text/plain"},
|
||||
// Keep this as native Tinode text content. Product-specific x-im
|
||||
// Keep this as native MeetingIM text content. Product-specific x-im
|
||||
// headers should be covered by a separate WS message-header matrix.
|
||||
"content": text,
|
||||
}}); err != nil {
|
||||
@@ -945,7 +945,7 @@ func (a *audit) call(method, path string, body any, token string) (int, imResp,
|
||||
return 0, imResp{}, err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Tinode-APIKey", a.apiKey)
|
||||
req.Header.Set("X-MeetingIM-APIKey", a.apiKey)
|
||||
req.Header.Set("X-Tenant-Code", a.tenantCode)
|
||||
req.Header.Set("X-IM-Schema", "1")
|
||||
req.Header.Set("X-Request-ID", "req-ws-audit-"+fmt.Sprintf("%d", time.Now().UnixNano()))
|
||||
@@ -954,7 +954,7 @@ func (a *audit) call(method, path string, body any, token string) (int, imResp,
|
||||
}
|
||||
if token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+token)
|
||||
req.Header.Set("X-Tinode-Auth", "token "+base64.StdEncoding.EncodeToString([]byte(token)))
|
||||
req.Header.Set("X-MeetingIM-Auth", "token "+base64.StdEncoding.EncodeToString([]byte(token)))
|
||||
}
|
||||
resp, err := a.client.Do(req)
|
||||
if err != nil {
|
||||
@@ -986,7 +986,7 @@ func (a *audit) report() string {
|
||||
fmt.Fprintf(&b, "- 用户 B:`%s` / `%s` / `%s`\n", a.userB, a.usernameB, a.phoneB)
|
||||
fmt.Fprintf(&b, "- API Key 与 Token 已脱敏,不在文档中展开。\n\n")
|
||||
fmt.Fprintf(&b, "## 2. 总体结论\n\n")
|
||||
fmt.Fprintf(&b, "本轮专项验证 Tinode WebSocket 真实协议链路,共执行 `%d` 项,通过 `%d` 项,未通过 `%d` 项。\n\n", total, passed, total-passed)
|
||||
fmt.Fprintf(&b, "本轮专项验证 MeetingIM WebSocket 真实协议链路,共执行 `%d` 项,通过 `%d` 项,未通过 `%d` 项。\n\n", total, passed, total-passed)
|
||||
fmt.Fprintf(&b, "| ID | 场景 | 方法 | 目标 | 结果 | 备注 |\n| --- | --- | --- | --- | --- | --- |\n")
|
||||
for _, r := range a.results {
|
||||
status := "通过"
|
||||
|
||||
+35
-34
@@ -29,7 +29,7 @@ API Key 是应用级接入标识,由部署人员在发布客户端前生成并
|
||||
|
||||
1. 新部署同时生成 API Key 和 HMAC Salt:在项目根目录执行 `go run ./keygen`。
|
||||
2. 已有服务端继续使用当前 `api_key_salt` 时,执行 `go run ./keygen -sequence <新序号> -salt '<服务端 api_key_salt>'` 生成兼容的普通应用 Key。
|
||||
3. 输出中的 `HMAC salt` 配置到服务端 `server/tinode.conf` 的 `api_key_salt`;输出中的 `API key` 配置到 iOS、Android 或 Web 客户端构建配置/受控环境配置。
|
||||
3. 输出中的 `HMAC salt` 配置到服务端 `server/meetingim.conf` 的 `api_key_salt`;输出中的 `API key` 配置到 iOS、Android 或 Web 客户端构建配置/受控环境配置。
|
||||
4. 客户端启动时从自己的应用配置读取 API Key,不调用 IM 接口获取。禁止把 `-isroot 1` 生成的 Root Key 放进客户端。
|
||||
|
||||
生成示意:
|
||||
@@ -44,7 +44,7 @@ API Key、企业码和用户 Token 是三个不同概念:
|
||||
|
||||
| 值 | 取得时间 | 作用 | 是否代表用户身份 |
|
||||
| --- | --- | --- | --- |
|
||||
| `api_key` | 应用发布/环境配置时预置 | 允许应用访问 Tinode 入口 | 否 |
|
||||
| `api_key` | 应用发布/环境配置时预置 | 允许应用访问 MeetingIM 入口 | 否 |
|
||||
| `tenant_code` | 用户输入企业码 | 将 Session 路由并绑定到企业 | 否 |
|
||||
| `token` | 用户登录成功后由服务端返回 | 标识当前租户内的登录用户 | 是 |
|
||||
|
||||
@@ -57,11 +57,11 @@ API Key 可以被客户端应用提取,因此不能把它当成用户密码或
|
||||
| 通道 | 地址 | 认证 | 用途 |
|
||||
| --- | --- | --- | --- |
|
||||
| WebSocket(浏览器/通用方式) | `wss://{host}/{api_path}v0/channels?apikey={api_key}` | Upgrade 校验 API Key,`hi` 绑定租户,`login` 登录 | Topic、消息、回执、在线状态、通话信令 |
|
||||
| WebSocket(支持自定义 Header 的原生客户端) | `wss://{host}/{api_path}v0/channels` | Upgrade Header `X-Tinode-APIKey` | 同上 |
|
||||
| WebSocket(支持自定义 Header 的原生客户端) | `wss://{host}/{api_path}v0/channels` | Upgrade Header `X-MeetingIM-APIKey` | 同上 |
|
||||
| 产品 HTTP | `https://{host}/{api_path}v1/im/*` | `Authorization: Bearer <params.token>` | 好友、群、收藏、搜索、工作台和设置 |
|
||||
| 文件 HTTP | `POST /v0/file/u/`、`GET /v0/file/s/{file_id}` | 当前用 `X-Tinode-Auth: token <base64_token>` | 文件上传下载;禁止 Query 传 Token |
|
||||
| 文件 HTTP | `POST /v0/file/u/`、`GET /v0/file/s/{file_id}` | 当前用 `X-MeetingIM-Auth: token <base64_token>` | 文件上传下载;禁止 Query 传 Token |
|
||||
|
||||
HTTP 请求统一通过 `X-Tinode-APIKey` 携带。浏览器 WebSocket 无法设置自定义 Upgrade Header 时才使用 `apikey` Query;日志和监控必须对该参数脱敏。产品 HTTP 还必须带 `X-IM-Schema: 1` 和 `X-Request-ID`;写操作带 `Idempotency-Key`。生产环境只允许 HTTPS/WSS。Token、密码、验证码、证件号不得写入 URL、普通日志、埋点或崩溃报告。
|
||||
HTTP 请求统一通过 `X-MeetingIM-APIKey` 携带。浏览器 WebSocket 无法设置自定义 Upgrade Header 时才使用 `apikey` Query;日志和监控必须对该参数脱敏。产品 HTTP 还必须带 `X-IM-Schema: 1` 和 `X-Request-ID`;写操作带 `Idempotency-Key`。生产环境只允许 HTTPS/WSS。Token、密码、验证码、证件号不得写入 URL、普通日志、埋点或崩溃报告。
|
||||
|
||||
WebSocket Upgrade 阶段由 `server/hdl_websock.go` 先调用 `checkAPIKey`。缺少或无效时不会建立 WebSocket,直接返回 HTTP 403:
|
||||
|
||||
@@ -221,7 +221,7 @@ WebSocket Upgrade 阶段由 `server/hdl_websock.go` 先调用 `checkAPIKey`。
|
||||
```http
|
||||
POST /v0/channels/lp HTTP/1.1
|
||||
Host: im.example.com
|
||||
X-Tinode-APIKey: <预置的普通 API Key>
|
||||
X-MeetingIM-APIKey: <预置的普通 API Key>
|
||||
Content-Length: 0
|
||||
```
|
||||
|
||||
@@ -235,7 +235,7 @@ Content-Length: 0
|
||||
|
||||
```http
|
||||
POST /v0/channels/lp?sid=session-id HTTP/1.1
|
||||
X-Tinode-APIKey: <预置的普通 API Key>
|
||||
X-MeetingIM-APIKey: <预置的普通 API Key>
|
||||
Content-Type: application/json
|
||||
|
||||
{"hi":{"id":"hi-1","ver":"0.25","ua":"android/1.0.0","platf":"android","lang":"zh-CN","tenant":"acme"}}
|
||||
@@ -245,7 +245,7 @@ POST 只负责把命令送入 Session。客户端再保持一个不带 Body 的
|
||||
|
||||
```http
|
||||
GET /v0/channels/lp?sid=session-id HTTP/1.1
|
||||
X-Tinode-APIKey: <预置的普通 API Key>
|
||||
X-MeetingIM-APIKey: <预置的普通 API Key>
|
||||
```
|
||||
|
||||
收到 `hi` 成功后,以相同方式 POST `{login}`,再 GET 登录结果;登录成功后依次 POST `{sub me}`、`{sub fnd}` 和会话订阅,并持续发起下一次 GET 等待下行消息。
|
||||
@@ -270,7 +270,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
本节用于 WebSocket 联调。除特别说明外:
|
||||
|
||||
- WebSocket 地址为 `wss://{host}/{api_path}v0/channels?apikey={api_key}`;支持自定义 Header 的原生客户端也可用 `X-Tinode-APIKey`。
|
||||
- WebSocket 地址为 `wss://{host}/{api_path}v0/channels?apikey={api_key}`;支持自定义 Header 的原生客户端也可用 `X-MeetingIM-APIKey`。
|
||||
- 连接建立后第一帧必须是 `{hi}`,`hi` 成功后才能 `{login}` 或 `{acc}`。
|
||||
- 每个上行命令都带客户端生成的 `id`,客户端用该 `id` 匹配 `{ctrl}`;同一连接内保持唯一即可。
|
||||
- 服务端下行可能乱序到达,客户端不能假设 `{ctrl}` 一定先于 `{meta}` 或 `{data}`。
|
||||
@@ -336,7 +336,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
##### 0.5.4.6 系统通知、好友和群业务下行
|
||||
|
||||
服务端主动下行分两类:Tinode 原生 Topic 消息使用 `{data}`、`{info}`、`{pres}` 等帧;IM 产品业务事件使用 `{info}` 帧下发,前端按 `info.what + info.event` 分发刷新。产品业务事件不占普通聊天 `seq`,不等同于系统通知列表记录;系统消息入口的未读数来自 `im_system_notifications` 和 `GET /v1/im/system-notifications`。
|
||||
服务端主动下行分两类:MeetingIM 原生 Topic 消息使用 `{data}`、`{info}`、`{pres}` 等帧;IM 产品业务事件使用 `{info}` 帧下发,前端按 `info.what + info.event` 分发刷新。产品业务事件不占普通聊天 `seq`,不等同于系统通知列表记录;系统消息入口的未读数来自 `im_system_notifications` 和 `GET /v1/im/system-notifications`。
|
||||
|
||||
用户维度业务事件统一格式:
|
||||
|
||||
@@ -425,8 +425,8 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
| WebSocket `login basic` | `{"login":{"id":"login-1","scheme":"basic","secret":"base64(username:password)"}}` | `{"ctrl":{"id":"login-1","code":200,"params":{"user":"usr...","token":"token...","expires":"2026-09-03T09:00:00Z","authlvl":"auth"}}}` |
|
||||
| WebSocket `acc new` | `{"acc":{"id":"acc-1","user":"new","scheme":"basic","username":"alice","phone":"15500000000","secret":"base64(alice:password)","login":true,"cred":[{"meth":"tel","val":"15500000000","resp":"verification_token","params":{"challenge_id":"chg_123"}}]}}` | `{"ctrl":{"id":"acc-1","code":200,"params":{"user":"usr...","username":"alice","token":"token...","expires":"2026-09-03T09:00:00Z"}}}` |
|
||||
| `POST /v0/channels/lp` | Long Polling 首包 `{"hi":{"id":"hi-1","ver":"0.25","tenant":"default"}}` | `{"ctrl":{"id":"hi-1","code":201,"params":{"sid":"lp_sid_123"}}}` |
|
||||
| `POST /v0/file/u/` | Multipart 文件;Header `X-Tinode-Auth: token <base64_token>`;业务表单可带 `purpose=chat_file&topic=grp...` | `{"ctrl":{"code":200,"params":{"url":"/v0/file/s/file123","name":"a.pdf","size":1024,"mime":"application/pdf"}}}` |
|
||||
| `GET /v0/file/s/{file_id}` | Header `X-Tinode-Auth: token <base64_token>`,可带 `Range: bytes=0-` | 返回文件字节流;失败返回 `FILE_FORBIDDEN` 或 `FILE_UNAVAILABLE` |
|
||||
| `POST /v0/file/u/` | Multipart 文件;Header `X-MeetingIM-Auth: token <base64_token>`;业务表单可带 `purpose=chat_file&topic=grp...` | `{"ctrl":{"code":200,"params":{"url":"/v0/file/s/file123","name":"a.pdf","size":1024,"mime":"application/pdf"}}}` |
|
||||
| `GET /v0/file/s/{file_id}` | Header `X-MeetingIM-Auth: token <base64_token>`,可带 `Range: bytes=0-` | 返回文件字节流;失败返回 `FILE_FORBIDDEN` 或 `FILE_UNAVAILABLE` |
|
||||
|
||||
### 0.6 Topic 与产品消息
|
||||
|
||||
@@ -481,7 +481,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
**企业解析接口:**
|
||||
|
||||
- `POST /v1/im/tenants/resolve` 未登录可调用,但必须带 `X-Tinode-APIKey` 和 `X-IM-Schema: 1`;Header 传 `X-Tenant-Code: acme`,Body 可传 `{"tenant_code":"acme","platform":"android","app_version":"1.0.0"}`。
|
||||
- `POST /v1/im/tenants/resolve` 未登录可调用,但必须带 `X-MeetingIM-APIKey` 和 `X-IM-Schema: 1`;Header 传 `X-Tenant-Code: acme`,Body 可传 `{"tenant_code":"acme","platform":"android","app_version":"1.0.0"}`。
|
||||
- 成功数据:`tenant{code,name,tenant_desc,branding}`、`endpoints{websocket,api,file}`、`auth_methods`、`password_policy`、`app_update`。不得返回 `tenant_id` 或 `tenant_ticket`。
|
||||
- 后续:保存公开企业信息,连接响应给出的 WebSocket 地址,仍然发送 0.5 节的 `hi(tenant)` 做最终租户绑定;`hi` 成功后才进入 1.2。
|
||||
- 目标失败协议:企业不存在、停用或不允许登录统一返回 `TENANT_UNAVAILABLE`;客户端不得区分不存在和停用。网络失败保留输入并允许重试。
|
||||
@@ -530,7 +530,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
- 可选字段:`license_file_id` 营业执照图片文件 ID,`enterprise_email` 企业邮箱。邮箱用于接收审核结果和后续通知,填写时必须符合邮箱格式。
|
||||
- 扫描识别:扫码或 OCR 只能作为填充辅助,提交前仍以输入框中的 `credit_code` 为准;识别失败不阻塞手动填写。
|
||||
- 图片上传:营业执照先调用公开用途受限上传接口,`purpose=enterprise_license`,支持 `jpg/png/webp/pdf`,单文件不超过 10MB。服务端通过当前 media handler 存储对象,生产环境必须配置 `oss` handler;上传成功返回 `file_id` 后再提交申请;未上传营业执照时 `license_file_id` 可为空。
|
||||
- 提交接口:`POST /v1/im/tenant-applications`,未登录可调用,必须带 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID` 和 `Idempotency-Key`。
|
||||
- 提交接口:`POST /v1/im/tenant-applications`,未登录可调用,必须带 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID` 和 `Idempotency-Key`。
|
||||
- 请求体:
|
||||
|
||||
```json
|
||||
@@ -581,7 +581,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
- 使用场景:企业码不可用、企业注册资料提交异常、审核通知未收到、登录入口无法进入等未登录阶段问题。
|
||||
- 输入字段:`description` 必填,长度 10~1000 字符;`contact` 可选,用于客服回访;`tenant_code_hint` 可选,来自用户输入;`screenshot_file_ids` 最多 9 个。
|
||||
- 截图上传:截图先调用公开用途受限上传接口,`purpose=tenant_entry_feedback`,支持 `jpg/png/webp`,单张不超过 10MB。服务端通过当前 media handler 存储对象,生产环境必须配置 `oss` handler;上传失败时允许单张重试或删除。
|
||||
- 提交接口:`POST /v1/im/tenant-entry-feedback`,未登录可调用,必须带 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID` 和 `Idempotency-Key`。
|
||||
- 提交接口:`POST /v1/im/tenant-entry-feedback`,未登录可调用,必须带 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID` 和 `Idempotency-Key`。
|
||||
- 请求体:
|
||||
|
||||
```json
|
||||
@@ -619,8 +619,8 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
- 前端动作:账号、密码非空且已勾选协议后启用登录;密码仅驻留当前页面内存。
|
||||
- 协议:已完成 `hi` 的 WebSocket 发送 `{login scheme:"basic"}`,`secret=base64(username:password)`;Base64 不是加密,必须通过 WSS 传输,不得先登录再传企业码。
|
||||
- 成功:读取 `{ctrl params.user/token/expires/authlvl}`,Token 存 Keychain/Keystore;立即 `sub me`,再加载会话。前端不解码 Token,也不再次 Base64 编码。
|
||||
- 产品 HTTP:使用同一个 `params.token` 作为 `Authorization: Bearer <params.token>`;所有 `/v1/im/*` HTTP 请求仍必须带 `X-Tinode-APIKey`、`X-IM-Schema: 1`,写请求再带 `Idempotency-Key`。
|
||||
- Token:当前复用 Tinode 单一登录 Token,默认有效期 14 天,不使用 Refresh Token。有效 Token 登录成功会签发新的 14 天 Token,客户端覆盖旧值;过期后重新账号密码登录。Token 已绑定 `tenant_id/user_id/auth_version`,修改密码、封禁账号后旧 Token 应失效。
|
||||
- 产品 HTTP:使用同一个 `params.token` 作为 `Authorization: Bearer <params.token>`;所有 `/v1/im/*` HTTP 请求仍必须带 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`,写请求再带 `Idempotency-Key`。
|
||||
- Token:当前复用 MeetingIM 单一登录 Token,默认有效期 14 天,不使用 Refresh Token。有效 Token 登录成功会签发新的 14 天 Token,客户端覆盖旧值;过期后重新账号密码登录。Token 已绑定 `tenant_id/user_id/auth_version`,修改密码、封禁账号后旧 Token 应失效。
|
||||
- 密码:服务端使用 `bcrypt.DefaultCost=10` 生成单向哈希,`im_auth.secret` 只保存哈希;客户端和服务端不得记录密码或完整 Basic `secret`。
|
||||
- 错误:`AUTH_FAILED` 统一显示账号或密码错误;`TENANT_MISMATCH` 清 Token 并回企业码页;`ACCOUNT_DISABLED`、`REALNAME_REQUIRED`、`YOUTH_RESTRICTED` 按专用页面处理。
|
||||
|
||||
@@ -778,7 +778,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
|
||||
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 写接口必须带 `Idempotency-Key`。二次认证接口按正文说明额外带 `X-Action-Token` 或 `X-Risk-Action-Token`。
|
||||
- 成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`;下表只展示 `data`。
|
||||
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}`。
|
||||
@@ -842,7 +842,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
**状态:基础消息操作部分已实现;产品权限矩阵需改造。**
|
||||
|
||||
- 长按本身不请求。客户端根据消息的 `type/from/seq/created_at/status`、当前 Topic 权限和服务端下发的 `message_policy` 预裁剪菜单。
|
||||
- 引用/复制为本地动作;收藏见 4.16;转发见 4.17~4.20;删除使用 Tinode `{del what:"msg",topic,delseq}`;撤回需新增 `POST /v1/im/topics/{topic}/messages/{seq}/recall`。
|
||||
- 引用/复制为本地动作;收藏见 4.16;转发见 4.17~4.20;删除使用 MeetingIM `{del what:"msg",topic,delseq}`;撤回需新增 `POST /v1/im/topics/{topic}/messages/{seq}/recall`。
|
||||
- 编辑使用 `POST /v1/im/topics/{topic}/messages/{seq}/revisions`,Body `{"content":...,"client_mid":"..."}`;仅文本且在时限内允许。
|
||||
- 群回执调用 `GET /v1/im/topics/{topic}/messages/{seq}/receipts?cursor=`。所有最终操作由服务端验证消息归属、时限、群角色和租户。
|
||||
|
||||
@@ -861,7 +861,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
|
||||
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 写接口必须带 `Idempotency-Key`。二次认证接口按正文说明额外带 `X-Action-Token` 或 `X-Risk-Action-Token`。
|
||||
- 成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`;下表只展示 `data`。
|
||||
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}`。
|
||||
@@ -1194,7 +1194,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
|
||||
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 写接口必须带 `Idempotency-Key`。二次认证接口按正文说明额外带 `X-Action-Token` 或 `X-Risk-Action-Token`。
|
||||
- 成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`;下表只展示 `data`。
|
||||
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}`。
|
||||
@@ -1361,7 +1361,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
**状态:图片文件名与画面语义不一致;按“清除聊天记录”实施,需改造。**
|
||||
|
||||
- 确认调用 Tinode `{del what:"msg",topic,delseq:[{"low":1,"hi":last_seq+1}],hard:false}`,仅删除当前用户视图;或封装 `DELETE /v1/im/conversations/{topic}/messages`。
|
||||
- 确认调用 MeetingIM `{del what:"msg",topic,delseq:[{"low":1,"hi":last_seq+1}],hard:false}`,仅删除当前用户视图;或封装 `DELETE /v1/im/conversations/{topic}/messages`。
|
||||
- 不解除好友、不影响对方历史、不删除收藏;附件仅在无任何有效引用且过保留期后回收。
|
||||
- 前端需在确认文案明确“仅清除自己的聊天记录”,成功后清本地 Topic 消息缓存并保留会话关系。
|
||||
|
||||
@@ -1369,7 +1369,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
|
||||
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 写接口必须带 `Idempotency-Key`。二次认证接口按正文说明额外带 `X-Action-Token` 或 `X-Risk-Action-Token`。
|
||||
- 成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`;下表只展示 `data`。
|
||||
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}`。
|
||||
@@ -1404,7 +1404,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
<img src="images/群聊.png" alt="群聊" width="360">
|
||||
|
||||
**状态:Tinode 群 Topic 已实现基础能力;产品群列表与配额需改造。**
|
||||
**状态:MeetingIM 群 Topic 已实现基础能力;产品群列表与配额需改造。**
|
||||
|
||||
- 列表:`GET /v1/im/groups?category=owned|managed|joined&query=&cursor=`,返回 `topic,name,avatar_file_id,member_count,my_role,updated_at`。
|
||||
- 创建:`POST /v1/im/groups`,Body `{"name":"...","description":"...","member_user_ids":[],"avatar_file_id":"..."}`;`description` 可省略或传空字符串,最长 200 字符;原子校验租户群数上限和群成员上限,返回 `topic,name,avatar_file_id,description,owner,member_count,version,permissions`。
|
||||
@@ -1677,7 +1677,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
|
||||
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 写接口必须带 `Idempotency-Key`。二次认证接口按正文说明额外带 `X-Action-Token` 或 `X-Risk-Action-Token`。
|
||||
- 成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`;下表只展示 `data`。
|
||||
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}`。
|
||||
@@ -1812,7 +1812,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
**状态:底层下载已实现租户校验;资源 ACL 需改造。**
|
||||
|
||||
- 客户端使用 `GET /v0/file/s/{file_id}`,Header 传 API Key 与 `X-Tinode-Auth: token <base64_token>`;禁止 `?auth=&secret=`。
|
||||
- 客户端使用 `GET /v0/file/s/{file_id}`,Header 传 API Key 与 `X-MeetingIM-Auth: token <base64_token>`;禁止 `?auth=&secret=`。
|
||||
- 服务端除 Token 租户外,还必须校验该用户通过消息、群文件或收藏拥有有效引用,并支持 `Range`、`ETag`、内容长度和安全文件名。
|
||||
- 下载前客户端校验本地空间和网络策略;`FILE_FORBIDDEN` 不自动重试,`FILE_UNAVAILABLE` 显示失效。
|
||||
|
||||
@@ -1885,7 +1885,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
|
||||
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 写接口必须带 `Idempotency-Key`。二次认证接口按正文说明额外带 `X-Action-Token` 或 `X-Risk-Action-Token`。
|
||||
- 成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`;下表只展示 `data`。
|
||||
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}`。
|
||||
@@ -2022,7 +2022,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
|
||||
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 写接口必须带 `Idempotency-Key`。二次认证接口按正文说明额外带 `X-Action-Token` 或 `X-Risk-Action-Token`。
|
||||
- 成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`;下表只展示 `data`。
|
||||
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}`。
|
||||
@@ -2059,6 +2059,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
- 根节点:`GET /v1/im/organization/departments?parent_id=root&cursor=`;子部门同接口传 `parent_id`;成员 `GET /v1/im/organization/departments/{id}/members?cursor=`。
|
||||
- 部门返回 `department_id,name,parent_id,child_count,member_count,version`;成员只返回当前查看人可见字段。
|
||||
- 全局搜索:`GET /v1/im/organization/search?q=&types=department,user&cursor=`。服务端执行组织范围授权,离职用户标记不可联系或从结果移除。
|
||||
- 组织架构可见范围由服务端按当前登录用户裁剪:上架测试账号可看全部组织;手机号注册账号默认只能看到自己与好友所在组织路径及成员;邀请码注册账号默认只能看到自己所在组织路径及本人。上架测试账号通过租户业务库 `im_org_visibility_overrides` 配置 `visibility_scope=all`,客户端不传可见范围参数。
|
||||
|
||||
### 9.3 会议通知-发起人状态.png
|
||||
|
||||
@@ -2173,7 +2174,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
|
||||
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 写接口必须带 `Idempotency-Key`。二次认证接口按正文说明额外带 `X-Action-Token` 或 `X-Risk-Action-Token`。
|
||||
- 成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`;下表只展示 `data`。
|
||||
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}`。
|
||||
@@ -2230,10 +2231,10 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
<img src="images/个人信息.png" alt="个人信息" width="360">
|
||||
|
||||
**状态:Tinode `me` 资料更新已有基础;目标产品接口需改造。**
|
||||
**状态:MeetingIM `me` 资料更新已有基础;目标产品接口需改造。**
|
||||
|
||||
- 查询 `GET /v1/im/me/profile` 返回 `avatar_file_id,nickname,birth_date,signature,gender,region,email,public_account,version,field_rules`;`email` 当前仅为资料字段,不用于登录或找回密码。
|
||||
- 更新统一使用 `PATCH /v1/im/me/profile`,仅提交变更字段并带 `version`;服务端更新后同步 Tinode `me.public` 并推送资料事件。
|
||||
- 更新统一使用 `PATCH /v1/im/me/profile`,仅提交变更字段并带 `version`;服务端更新后同步 MeetingIM `me.public` 并推送资料事件。
|
||||
- 头像先以 `purpose=user_avatar` 上传再 PATCH;任何字段的可见性由隐私策略裁剪。
|
||||
|
||||
### 10.3 设置昵称.png
|
||||
@@ -2420,7 +2421,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
|
||||
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 写接口必须带 `Idempotency-Key`。二次认证接口按正文说明额外带 `X-Action-Token` 或 `X-Risk-Action-Token`。
|
||||
- 成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`;下表只展示 `data`。
|
||||
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}`。
|
||||
@@ -2505,7 +2506,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
以下示例用于本章 HTTP 联调快速查阅。除特别说明外:
|
||||
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-Tinode-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 产品 HTTP 均使用 `https://{host}/{api_path}v1/im`,请求头包含 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID`;登录后接口再带 `Authorization: Bearer <token>`。
|
||||
- 写接口必须带 `Idempotency-Key`。二次认证接口按正文说明额外带 `X-Action-Token` 或 `X-Risk-Action-Token`。
|
||||
- 成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`;下表只展示 `data`。
|
||||
- 失败响应统一包裹为 `{"code":30000,"reason":"MALFORMED_REQUEST","message":"...","details":{},"request_id":"...","server_time":"..."}`。
|
||||
@@ -2527,7 +2528,7 @@ X-Tinode-APIKey: <预置的普通 API Key>
|
||||
|
||||
1. WebSocket `/v0/channels`、Wire Version `0.25` 和最低版本 `0.20`。
|
||||
2. 第一条 `hi` 携带企业码并固定 Session 租户;响应包含公开租户信息、限制、ICE 配置和呼叫超时。
|
||||
3. `basic`/`token` 登录,`sub/get/set/pub/leave/del/note` 基础 Tinode 协议。
|
||||
3. `basic`/`token` 登录,`sub/get/set/pub/leave/del/note` 基础 MeetingIM 协议。
|
||||
4. `pub` 成功返回 `202 + seq`,Topic 消息与基础回执同步。
|
||||
5. `/v0/file/u/` 和 `/v0/file/s/{file_id}` 的基础上传下载及租户存储隔离。
|
||||
|
||||
|
||||
@@ -443,6 +443,19 @@ CREATE TABLE IF NOT EXISTS tinode.im_org_members (
|
||||
CONSTRAINT fk_im_org_members_department FOREIGN KEY (department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_org_visibility_overrides (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
visibility_scope VARCHAR(16) NOT NULL,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
remark VARCHAR(256) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_org_visibility_user (user_id),
|
||||
INDEX idx_im_org_visibility_scope (visibility_scope, state)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_meetings (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
meeting_id VARCHAR(64) NOT NULL,
|
||||
|
||||
+2
-1
@@ -280,6 +280,7 @@
|
||||
10. 会议创建、变更、取消、提醒和参会回应已生成在线事件与系统通知,关键变更在事务内写 Outbox。
|
||||
11. 组织全局成员接口 `GET /v1/im/organization/members` 已具备。
|
||||
12. 月报 reopen `POST /v1/im/reports/monthly/{id}/reopen` 已具备。
|
||||
13. 组织架构可见范围已按账号来源裁剪:邀请码注册默认仅本人,手机号注册默认本人和好友组织路径,上架测试账号可通过 `im_org_visibility_overrides.visibility_scope=all` 放开全量。
|
||||
|
||||
### 9.2 后续增强项
|
||||
|
||||
@@ -288,7 +289,7 @@
|
||||
| P0 | 工作台权限与功能开关配置化 | 当前模块基线可返回,后续需要按租户、部门、角色、后台配置动态裁剪。 | 不同用户登录同一租户看到不同授权模块。 |
|
||||
| P0 | 工作台未读角标实时事件 | `workbench_total_unread` 已有,后续需要模块未读聚合和 `workbench.badge.updated` 增量事件。 | 会议、日报、信箱未读变化后工作台角标实时更新。 |
|
||||
| P0 | 组织数据后台维护链路 | 当前前台读接口和表已具备,后续需要后台导入、增删改部门、调整成员和离职状态。 | 后台修改组织后前台接口版本变化,客户端可刷新。 |
|
||||
| P0 | 组织权限与可见字段裁剪 | 成员手机号、邮箱、职位等字段需要按组织角色、隐私和企业策略裁剪。 | 普通员工和管理员看到的成员字段不同且不可绕过。 |
|
||||
| P0 | 组织字段级隐私策略 | 组织架构范围裁剪已具备,成员手机号、邮箱、职位等字段后续仍需按组织角色、隐私和企业策略进一步细化。 | 普通员工和管理员看到的成员字段不同且不可绕过。 |
|
||||
| P1 | 组织全局成员接口深度联调 | `GET /v1/im/organization/members` 已具备,仍需按部门、关键词、权限和分页做测试环境回归。 | 前端可直接分页查询并按部门过滤,字段裁剪符合权限。 |
|
||||
| P1 | 会议列表完整联调 | 对接文档标注 `GET /v1/im/meetings?cursor=` 待补列表;代码有 `serveIMMeetings`,仍需用测试数据确认返回结构。 | 会议列表、详情、响应、取消端到端一致。 |
|
||||
| P1 | 月报 reopen 审计与权限扩展 | reopen 接口已具备,后续需要后台权限、原因记录、审计和通知闭环。 | 已提交月报可按权限退回草稿并保留审计。 |
|
||||
|
||||
+1
-1
@@ -24,7 +24,7 @@
|
||||
|
||||
<request subst="true">
|
||||
<websocket type="connect" path="/v0/channels?apikey=AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K">
|
||||
<!--http_header name="X-Tinode-APIKey" value="AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K"/-->
|
||||
<!--http_header name="X-MeetingIM-APIKey" value="AQEAAAABAAD_rAp4DJh05a1HAwFT3A6K"/-->
|
||||
</websocket>
|
||||
</request>
|
||||
|
||||
|
||||
@@ -275,6 +275,9 @@ func (a *adapter) Open(jsonconfig json.RawMessage) error {
|
||||
if err = ensureIMFileReviewTables(a.db); err != nil {
|
||||
return errors.New("mysql adapter failed to ensure file review schema: " + err.Error())
|
||||
}
|
||||
if err = ensureIMOrganizationVisibilityTables(a.db); err != nil {
|
||||
return errors.New("mysql adapter failed to ensure organization visibility schema: " + err.Error())
|
||||
}
|
||||
if err = ensureSnowflakePrimaryKeys(a.db); err != nil {
|
||||
return errors.New("mysql adapter failed to ensure snowflake primary keys: " + err.Error())
|
||||
}
|
||||
@@ -507,6 +510,10 @@ func (a *adapter) TenantAdapterFor(tenantID t.TenantID) (dbadapter.TenantBusines
|
||||
db.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
if err = ensureIMOrganizationVisibilityTables(db); err != nil {
|
||||
db.Close()
|
||||
return nil, nil, err
|
||||
}
|
||||
if err = ensureSnowflakePrimaryKeys(db); err != nil {
|
||||
db.Close()
|
||||
return nil, nil, err
|
||||
@@ -1187,6 +1194,18 @@ func createIMOrganizationTables(tx *sql.Tx) error {
|
||||
INDEX idx_im_org_members_display_name(display_name),
|
||||
CONSTRAINT fk_im_org_members_department FOREIGN KEY(department_id) REFERENCES im_org_departments(department_id) ON DELETE CASCADE
|
||||
)`,
|
||||
`CREATE TABLE im_org_visibility_overrides(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
visibility_scope VARCHAR(16) NOT NULL,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
remark VARCHAR(256) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_org_visibility_user(user_id),
|
||||
INDEX idx_im_org_visibility_scope(visibility_scope,state)
|
||||
)`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if _, err := tx.Exec(stmt); err != nil {
|
||||
@@ -1196,6 +1215,22 @@ func createIMOrganizationTables(tx *sql.Tx) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureIMOrganizationVisibilityTables(exec sqlExecutor) error {
|
||||
_, err := exec.Exec(`CREATE TABLE IF NOT EXISTS im_org_visibility_overrides(
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
visibility_scope VARCHAR(16) NOT NULL,
|
||||
state VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
remark VARCHAR(256) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY(id),
|
||||
UNIQUE INDEX uk_im_org_visibility_user(user_id),
|
||||
INDEX idx_im_org_visibility_scope(visibility_scope,state)
|
||||
)`)
|
||||
return err
|
||||
}
|
||||
|
||||
func createIMMeetingTables(tx *sql.Tx) error {
|
||||
statements := []string{
|
||||
`CREATE TABLE im_meetings(
|
||||
|
||||
@@ -2681,12 +2681,28 @@ func (a *adapter) TenantOrganizationDepartments(id t.TenantID, uid t.Uid, parent
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
|
||||
(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active') AS child_count,
|
||||
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id) AS member_count
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
cte, cteArgs := visibility.departmentCTE()
|
||||
memberCountSQL, memberCountArgs := visibility.memberCountSQL("m")
|
||||
if visibility.Scope != orgVisibilityAll {
|
||||
where = append(where, "d.department_id IN (SELECT department_id FROM visible_depts)")
|
||||
}
|
||||
queryArgs := append([]any{}, cteArgs...)
|
||||
queryArgs = append(queryArgs, memberCountArgs...)
|
||||
queryArgs = append(queryArgs, args...)
|
||||
childCountSQL := `(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active')`
|
||||
if visibility.Scope != orgVisibilityAll {
|
||||
childCountSQL = `(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active' AND child.department_id IN (SELECT department_id FROM visible_depts))`
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, cte+`SELECT d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
|
||||
`+childCountSQL+` AS child_count,
|
||||
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id AND m.employment_state='active'`+memberCountSQL+`) AS member_count
|
||||
FROM im_org_departments AS d
|
||||
WHERE `+strings.Join(where, " AND ")+`
|
||||
ORDER BY d.sort_order ASC,d.id ASC LIMIT ?`, args...)
|
||||
ORDER BY d.sort_order ASC,d.id ASC LIMIT ?`, queryArgs...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
@@ -2726,7 +2742,7 @@ func (a *adapter) TenantOrganizationMembers(id t.TenantID, uid t.Uid, department
|
||||
if limit <= 0 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
where := []string{"m.department_id=?"}
|
||||
where := []string{"m.department_id=?", "m.employment_state='active'"}
|
||||
args := []any{strings.TrimSpace(departmentID)}
|
||||
if cursor != "" {
|
||||
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
||||
@@ -2741,6 +2757,15 @@ func (a *adapter) TenantOrganizationMembers(id t.TenantID, uid t.Uid, department
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
|
||||
if visibilitySQL != "" {
|
||||
where = append(where, visibilitySQL)
|
||||
args = append(args, visibilityArgs...)
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT m.id,m.department_id,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,
|
||||
m.employment_state,m.contactable,m.sort_order,m.version,m.created_at,m.updated_at
|
||||
FROM im_org_members AS m
|
||||
@@ -2809,6 +2834,15 @@ func (a *adapter) TenantOrganizationMembersAll(id t.TenantID, uid t.Uid, departm
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
|
||||
if visibilitySQL != "" {
|
||||
where = append(where, visibilitySQL)
|
||||
args = append(args, visibilityArgs...)
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT m.id,m.department_id,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,
|
||||
m.employment_state,m.contactable,m.sort_order,m.version,m.created_at,m.updated_at
|
||||
FROM im_org_members AS m
|
||||
@@ -2866,29 +2900,45 @@ func (a *adapter) TenantOrganizationSearch(id t.TenantID, uid t.Uid, queryText,
|
||||
return nil, "", t.ErrMalformed
|
||||
}
|
||||
like := "%" + strings.TrimSpace(queryText) + "%"
|
||||
parts := make([]string, 0, 2)
|
||||
args := make([]any, 0, 6)
|
||||
if includeDepartments {
|
||||
parts = append(parts, `SELECT 'department' AS result_type,d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
|
||||
(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active') AS child_count,
|
||||
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id) AS member_count,
|
||||
0 AS user_id,'' AS display_name,'' AS position,'' AS avatar_file_id,'' AS mobile,'' AS email,'' AS employment_state,0 AS contactable
|
||||
FROM im_org_departments AS d WHERE d.state='active' AND d.name LIKE ?`)
|
||||
args = append(args, like)
|
||||
}
|
||||
if includeUsers {
|
||||
parts = append(parts, `SELECT 'user' AS result_type,m.id,'' AS department_id,'' AS name,m.department_id AS parent_id,m.sort_order,m.version,'' AS state,m.created_at,m.updated_at,
|
||||
0 AS child_count,0 AS member_count,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,m.employment_state,m.contactable
|
||||
FROM im_org_members AS m
|
||||
WHERE m.display_name LIKE ? OR m.mobile LIKE ? OR m.email LIKE ? OR CAST(m.user_id AS CHAR) LIKE ?`)
|
||||
args = append(args, like, like, like, like)
|
||||
}
|
||||
args = append(args, offset, limit+1)
|
||||
ctx, cancel := a.getContext()
|
||||
if cancel != nil {
|
||||
defer cancel()
|
||||
}
|
||||
rows, err := a.db.QueryxContext(ctx, `SELECT * FROM (`+strings.Join(parts, " UNION ALL ")+`) AS results
|
||||
visibility, err := a.organizationVisibility(ctx, uid)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
cte, cteArgs := visibility.departmentCTE()
|
||||
parts := make([]string, 0, 2)
|
||||
args := make([]any, 0, 10)
|
||||
args = append(args, cteArgs...)
|
||||
if includeDepartments {
|
||||
departmentWhere := "d.state='active' AND d.name LIKE ?"
|
||||
if visibility.Scope != orgVisibilityAll {
|
||||
departmentWhere += " AND d.department_id IN (SELECT department_id FROM visible_depts)"
|
||||
}
|
||||
parts = append(parts, `SELECT 'department' AS result_type,d.id,d.department_id,d.name,d.parent_id,d.sort_order,d.version,d.state,d.created_at,d.updated_at,
|
||||
(SELECT COUNT(*) FROM im_org_departments AS child WHERE child.parent_id=d.department_id AND child.state='active') AS child_count,
|
||||
(SELECT COUNT(*) FROM im_org_members AS m WHERE m.department_id=d.department_id) AS member_count,
|
||||
0 AS user_id,'' AS display_name,'' AS position,'' AS avatar_file_id,'' AS mobile,'' AS email,'' AS employment_state,0 AS contactable
|
||||
FROM im_org_departments AS d WHERE `+departmentWhere)
|
||||
args = append(args, like)
|
||||
}
|
||||
if includeUsers {
|
||||
userWhere := "(m.display_name LIKE ? OR m.mobile LIKE ? OR m.email LIKE ? OR CAST(m.user_id AS CHAR) LIKE ?) AND m.employment_state='active'"
|
||||
visibilitySQL, visibilityArgs := visibility.memberWhereSQL("m")
|
||||
if visibilitySQL != "" {
|
||||
userWhere += " AND " + visibilitySQL
|
||||
}
|
||||
parts = append(parts, `SELECT 'user' AS result_type,m.id,'' AS department_id,'' AS name,m.department_id AS parent_id,m.sort_order,m.version,'' AS state,m.created_at,m.updated_at,
|
||||
0 AS child_count,0 AS member_count,m.user_id,m.display_name,m.position,m.avatar_file_id,m.mobile,m.email,m.employment_state,m.contactable
|
||||
FROM im_org_members AS m
|
||||
WHERE `+userWhere)
|
||||
args = append(args, like, like, like, like)
|
||||
args = append(args, visibilityArgs...)
|
||||
}
|
||||
args = append(args, offset, limit+1)
|
||||
rows, err := a.db.QueryxContext(ctx, cte+`SELECT * FROM (`+strings.Join(parts, " UNION ALL ")+`) AS results
|
||||
ORDER BY result_type ASC,id ASC LIMIT ?,?`, args...)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
@@ -2913,6 +2963,106 @@ func (a *adapter) TenantOrganizationSearch(id t.TenantID, uid t.Uid, queryText,
|
||||
return items, nextCursor, nil
|
||||
}
|
||||
|
||||
const (
|
||||
orgVisibilityAll = "all"
|
||||
orgVisibilityFriends = "friends"
|
||||
orgVisibilitySelf = "self"
|
||||
)
|
||||
|
||||
type organizationVisibility struct {
|
||||
Scope string
|
||||
UserID int64
|
||||
}
|
||||
|
||||
func (a *adapter) organizationVisibility(ctx context.Context, uid t.Uid) (organizationVisibility, error) {
|
||||
userID := decodeUID(uid)
|
||||
visibility := organizationVisibility{Scope: orgVisibilitySelf, UserID: userID}
|
||||
if userID <= 0 {
|
||||
return visibility, t.ErrMalformed
|
||||
}
|
||||
|
||||
var override string
|
||||
err := a.db.QueryRowxContext(ctx, `SELECT visibility_scope FROM im_org_visibility_overrides
|
||||
WHERE user_id=? AND state='active' LIMIT 1`, userID).Scan(&override)
|
||||
if err != nil && err != sql.ErrNoRows {
|
||||
return visibility, err
|
||||
}
|
||||
if err == nil {
|
||||
switch strings.TrimSpace(override) {
|
||||
case orgVisibilityAll, orgVisibilityFriends, orgVisibilitySelf:
|
||||
visibility.Scope = strings.TrimSpace(override)
|
||||
return visibility, nil
|
||||
default:
|
||||
return visibility, t.ErrMalformed
|
||||
}
|
||||
}
|
||||
|
||||
var phone sql.NullString
|
||||
err = a.db.QueryRowxContext(ctx, `SELECT phone FROM im_users WHERE id=? AND state<>? LIMIT 1`, userID, t.StateDeleted).Scan(&phone)
|
||||
if err == sql.ErrNoRows {
|
||||
return visibility, t.ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return visibility, err
|
||||
}
|
||||
var inviteUsed int
|
||||
err = a.db.QueryRowxContext(ctx, `SELECT EXISTS(SELECT 1 FROM im_registration_invite_uses WHERE user_id=?)`, userID).Scan(&inviteUsed)
|
||||
if err != nil {
|
||||
return visibility, err
|
||||
}
|
||||
if inviteUsed > 0 {
|
||||
visibility.Scope = orgVisibilitySelf
|
||||
return visibility, nil
|
||||
}
|
||||
if phone.Valid && strings.TrimSpace(phone.String) != "" {
|
||||
visibility.Scope = orgVisibilityFriends
|
||||
}
|
||||
return visibility, nil
|
||||
}
|
||||
|
||||
func (v organizationVisibility) memberWhereSQL(alias string) (string, []any) {
|
||||
alias = strings.TrimSpace(alias)
|
||||
if alias == "" {
|
||||
alias = "m"
|
||||
}
|
||||
switch v.Scope {
|
||||
case orgVisibilityAll:
|
||||
return "", nil
|
||||
case orgVisibilityFriends:
|
||||
return "(" + alias + ".user_id=? OR EXISTS (SELECT 1 FROM im_friendships AS fs WHERE fs.user_id=? AND fs.peer_id=" + alias + ".user_id AND fs.status=?))",
|
||||
[]any{v.UserID, v.UserID, t.RelationshipFriend}
|
||||
default:
|
||||
return alias + ".user_id=?", []any{v.UserID}
|
||||
}
|
||||
}
|
||||
|
||||
func (v organizationVisibility) memberCountSQL(alias string) (string, []any) {
|
||||
where, args := v.memberWhereSQL(alias)
|
||||
if where == "" {
|
||||
return "", nil
|
||||
}
|
||||
return " AND " + where, args
|
||||
}
|
||||
|
||||
func (v organizationVisibility) departmentCTE() (string, []any) {
|
||||
if v.Scope == orgVisibilityAll {
|
||||
return "", nil
|
||||
}
|
||||
memberWhere, args := v.memberWhereSQL("m")
|
||||
return `WITH RECURSIVE visible_member_depts AS (
|
||||
SELECT DISTINCT m.department_id FROM im_org_members AS m
|
||||
WHERE m.employment_state='active' AND ` + memberWhere + `
|
||||
), visible_depts AS (
|
||||
SELECT d.department_id,d.parent_id FROM im_org_departments AS d
|
||||
JOIN visible_member_depts AS vmd ON vmd.department_id=d.department_id
|
||||
WHERE d.state='active'
|
||||
UNION DISTINCT
|
||||
SELECT parent.department_id,parent.parent_id FROM im_org_departments AS parent
|
||||
JOIN visible_depts AS child ON child.parent_id=parent.department_id
|
||||
WHERE parent.state='active'
|
||||
) `, args
|
||||
}
|
||||
|
||||
func organizationSearchIncludes(resultTypes string) (bool, bool) {
|
||||
if strings.TrimSpace(resultTypes) == "" {
|
||||
return true, true
|
||||
|
||||
+3
-3
@@ -300,7 +300,7 @@ func cacheControlHandler(maxAge int, handler http.Handler) http.Handler {
|
||||
// Get API key from an HTTP request.
|
||||
func getAPIKey(req *http.Request) string {
|
||||
// Check header.
|
||||
apikey := req.Header.Get("X-Tinode-APIKey")
|
||||
apikey := req.Header.Get("X-MeetingIM-APIKey")
|
||||
if apikey != "" {
|
||||
return apikey
|
||||
}
|
||||
@@ -328,8 +328,8 @@ func getAPIKey(req *http.Request) string {
|
||||
// Extracts authorization credentials from an HTTP request.
|
||||
// Returns authentication method and secret.
|
||||
func getHttpAuth(req *http.Request) (method, secret string) {
|
||||
// Check X-Tinode-Auth header.
|
||||
if parts := strings.Split(req.Header.Get("X-Tinode-Auth"), " "); len(parts) == 2 {
|
||||
// Check MeetingIM product auth header.
|
||||
if parts := strings.Split(req.Header.Get("X-MeetingIM-Auth"), " "); len(parts) == 2 {
|
||||
method, secret = parts[0], parts[1]
|
||||
return
|
||||
}
|
||||
|
||||
+18
-18
@@ -110,7 +110,7 @@ func withIMHTTPProtocol(prefix string, next http.Handler) http.Handler {
|
||||
wrt.Header().Set("X-Request-ID", requestID)
|
||||
wrt.Header().Set("X-IM-Schema", "1")
|
||||
|
||||
if valid, _ := checkAPIKey(strings.TrimSpace(req.Header.Get("X-Tinode-APIKey"))); !valid {
|
||||
if valid, _ := checkAPIKey(strings.TrimSpace(getAPIKey(req))); !valid {
|
||||
writeIMError(wrt, req, http.StatusForbidden, 30003, imReasonAPIKeyRequired, "valid API key required", nil)
|
||||
return
|
||||
}
|
||||
@@ -7283,24 +7283,24 @@ func meetingPayloadWithUserSummaries(meeting *types.Meeting, viewer types.Uid, u
|
||||
startAt := meeting.StartAt.UTC()
|
||||
endAt := meeting.EndAt.UTC()
|
||||
data := map[string]any{
|
||||
"meeting_id": meeting.MeetingID,
|
||||
"title": meeting.Title,
|
||||
"organizer_id": meeting.OrganizerID.UserId(),
|
||||
"organizer": organizer,
|
||||
"start_at": startAt.Format(time.RFC3339Nano),
|
||||
"end_at": endAt.Format(time.RFC3339Nano),
|
||||
"meeting_time": meetingTimeRange(startAt, endAt),
|
||||
"time_range": meetingTimeRange(startAt, endAt),
|
||||
"location": meeting.Location,
|
||||
"state": meeting.State,
|
||||
"reminder_minutes": meeting.ReminderMinutes,
|
||||
"version": meeting.Version,
|
||||
"participants": participants,
|
||||
"meeting_id": meeting.MeetingID,
|
||||
"title": meeting.Title,
|
||||
"organizer_id": meeting.OrganizerID.UserId(),
|
||||
"organizer": organizer,
|
||||
"start_at": startAt.Format(time.RFC3339Nano),
|
||||
"end_at": endAt.Format(time.RFC3339Nano),
|
||||
"meeting_time": meetingTimeRange(startAt, endAt),
|
||||
"time_range": meetingTimeRange(startAt, endAt),
|
||||
"location": meeting.Location,
|
||||
"state": meeting.State,
|
||||
"reminder_minutes": meeting.ReminderMinutes,
|
||||
"version": meeting.Version,
|
||||
"participants": participants,
|
||||
"participant_count": len(participants),
|
||||
"participant_names": meetingParticipantNames(participants),
|
||||
"permissions": meetingPermissionsPayload(meeting, viewer),
|
||||
"created_at": meeting.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"updated_at": meeting.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"permissions": meetingPermissionsPayload(meeting, viewer),
|
||||
"created_at": meeting.CreatedAt.UTC().Format(time.RFC3339Nano),
|
||||
"updated_at": meeting.UpdatedAt.UTC().Format(time.RFC3339Nano),
|
||||
}
|
||||
if displayName, ok := organizer["display_name"]; ok {
|
||||
data["organizer_display_name"] = displayName
|
||||
@@ -7681,7 +7681,7 @@ func imCanViewLastSeen(tenantID types.TenantID, viewer, target types.Uid) bool {
|
||||
if viewer == target {
|
||||
return true
|
||||
}
|
||||
// Legacy deployments without the product privacy stores retain Tinode's
|
||||
// Legacy deployments without the product privacy stores retain MeetingIM's
|
||||
// original presence behavior.
|
||||
if store.Profiles == nil || store.Relations == nil {
|
||||
return true
|
||||
|
||||
+41
-18
@@ -1226,7 +1226,7 @@ func TestIMHTTPProtocolHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/v1/im/bootstrap", nil)
|
||||
req.Header.Set("X-Tinode-APIKey", apiKey)
|
||||
req.Header.Set("X-MeetingIM-APIKey", apiKey)
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusBadRequest || decodeIMHTTPResponse(t, rec).Reason != imReasonSchemaUnsupported {
|
||||
@@ -1234,7 +1234,7 @@ func TestIMHTTPProtocolHeaders(t *testing.T) {
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/v1/im/bootstrap", nil)
|
||||
req.Header.Set("X-Tinode-APIKey", apiKey)
|
||||
req.Header.Set("X-MeetingIM-APIKey", apiKey)
|
||||
req.Header.Set("X-IM-Schema", "1")
|
||||
req.Header.Set("X-Request-ID", "request-1")
|
||||
rec = httptest.NewRecorder()
|
||||
@@ -1242,6 +1242,29 @@ func TestIMHTTPProtocolHeaders(t *testing.T) {
|
||||
if rec.Code != http.StatusOK || called != 1 || rec.Header().Get("X-Request-ID") != "request-1" {
|
||||
t.Fatalf("valid protocol request: status=%d called=%d request_id=%q", rec.Code, called, rec.Header().Get("X-Request-ID"))
|
||||
}
|
||||
|
||||
req = httptest.NewRequest(http.MethodGet, "/v1/im/bootstrap", nil)
|
||||
req.Header.Set("X-MeetingIM-APIKey", apiKey)
|
||||
req.Header.Set("X-IM-Schema", "1")
|
||||
rec = httptest.NewRecorder()
|
||||
handler.ServeHTTP(rec, req)
|
||||
if rec.Code != http.StatusOK || called != 2 {
|
||||
t.Fatalf("legacy protocol request: status=%d called=%d", rec.Code, called)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingIMHTTPAuthHeaders(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/v0/file/s/file1", nil)
|
||||
req.Header.Set("X-MeetingIM-APIKey", "new-api-key")
|
||||
req.Header.Set("X-MeetingIM-Auth", "token new-token")
|
||||
|
||||
if got := getAPIKey(req); got != "new-api-key" {
|
||||
t.Fatalf("getAPIKey new header=%q", got)
|
||||
}
|
||||
method, secret := getHttpAuth(req)
|
||||
if method != "token" || secret != "new-token" {
|
||||
t.Fatalf("getHttpAuth new header method=%q secret=%q", method, secret)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIMHTTPIdempotencyReplayAndConflict(t *testing.T) {
|
||||
@@ -1263,7 +1286,7 @@ func TestIMHTTPIdempotencyReplayAndConflict(t *testing.T) {
|
||||
request := func(body string) *httptest.ResponseRecorder {
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/test", strings.NewReader(body))
|
||||
req.Header.Set("Authorization", "Bearer token-1")
|
||||
req.Header.Set("X-Tinode-APIKey", apiKey)
|
||||
req.Header.Set("X-MeetingIM-APIKey", apiKey)
|
||||
req.Header.Set("X-IM-Schema", "1")
|
||||
req.Header.Set("Idempotency-Key", "same-key")
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -1298,7 +1321,7 @@ func TestServeIMTenantResolveSuccessFromHeader(t *testing.T) {
|
||||
globals.servingAt = "https://im.example.com/"
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve", nil)
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", " acme ")
|
||||
req.Header.Set("X-Request-ID", "req-1")
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -1331,7 +1354,7 @@ func TestIMHTTPProtocolDoesNotRequireIdempotencyForTenantResolve(t *testing.T) {
|
||||
writeIMSuccess(w, req, map[string]any{"ok": true})
|
||||
})
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve", nil)
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-IM-Schema", "1")
|
||||
req.Header.Set("Authorization", "Bearer test-token")
|
||||
rec := httptest.NewRecorder()
|
||||
@@ -1362,7 +1385,7 @@ func TestServeIMTenantResolveUsesForwardedPublicHost(t *testing.T) {
|
||||
req.Host = "internal:6060"
|
||||
req.Header.Set("X-Forwarded-Proto", "https")
|
||||
req.Header.Set("X-Forwarded-Host", "api.118im.com")
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -1393,7 +1416,7 @@ func TestServeIMTenantResolveSuccessFromBody(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
|
||||
bytes.NewBufferString(`{"tenant_code":"acme","platform":"android","app_version":"1.0.0"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMTenantResolve(rec, req)
|
||||
@@ -1441,7 +1464,7 @@ func TestServeIMTenantResolveRequiresAPIKey(t *testing.T) {
|
||||
func TestServeIMTenantResolveRejectsMalformedTenantCode(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
|
||||
bytes.NewBufferString(`{"tenant_code":"`+strings.Repeat("a", 65)+`"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMTenantResolve(rec, req)
|
||||
@@ -1467,7 +1490,7 @@ func TestServeIMTenantResolveHidesUnavailableTenantState(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
|
||||
bytes.NewBufferString(`{"tenant_code":"acme"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMTenantResolve(rec, req)
|
||||
@@ -1488,7 +1511,7 @@ func TestServeIMTenantResolveHidesMissingTenant(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/tenants/resolve",
|
||||
bytes.NewBufferString(`{"tenant_code":"acme"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMTenantResolve(rec, req)
|
||||
@@ -1639,7 +1662,7 @@ func TestServeIMVerificationCodesCreatesChallenge(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
|
||||
bytes.NewBufferString(`{"scene":"register","phone":"+8613800138000"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -1689,7 +1712,7 @@ func TestServeIMVerificationCodesSkipsDeliveryWhenRegistrationSMSDisabled(t *tes
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
|
||||
bytes.NewBufferString(`{"scene":"register","phone":"+8613800138000"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -1729,7 +1752,7 @@ func TestServeIMPasswordResetCodeFindsPhoneWithoutTelCredential(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
|
||||
bytes.NewBufferString(`{"scene":"reset_password","phone":"13800138000"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -1768,7 +1791,7 @@ func TestServeIMVerificationCodesKeepsChallengeWhenDeliveryUnavailable(t *testin
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
|
||||
bytes.NewBufferString(`{"scene":"reset_password","phone":"+8613800138000"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -1786,7 +1809,7 @@ func TestServeIMVerificationCodesKeepsChallengeWhenDeliveryUnavailable(t *testin
|
||||
func TestServeIMVerificationCodesRequiresTenantCode(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
|
||||
bytes.NewBufferString(`{"scene":"register","phone":"+8613800138000"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
serveIMVerificationCodes(rec, req)
|
||||
@@ -1825,7 +1848,7 @@ func TestServeIMVerificationCodesRateLimited(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes",
|
||||
bytes.NewBufferString(`{"scene":"register","phone":"+8613800138000"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -1868,7 +1891,7 @@ func TestServeIMVerificationCodeVerifySuccess(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes/challenge-1/verify",
|
||||
bytes.NewBufferString(`{"code":"123456"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -1913,7 +1936,7 @@ func TestServeIMVerificationCodeVerifyInvalidCode(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/verification-codes/challenge-1/verify",
|
||||
bytes.NewBufferString(`{"code":"654321"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
|
||||
+1
-1
@@ -341,7 +341,7 @@ func main() {
|
||||
|
||||
logFlags := flag.String("log_flags", "stdFlags",
|
||||
"Comma-separated list of log flags (as defined in https://golang.org/pkg/log/#pkg-constants without the L prefix)")
|
||||
configfile := flag.String("config", "tinode.conf", "Path to config file.")
|
||||
configfile := flag.String("config", "meetingim.conf", "Path to config file.")
|
||||
// Path to static content.
|
||||
staticPath := flag.String("static_data", defaultStaticPath, "File path to directory with static files to be served.")
|
||||
listenOn := flag.String("listen", "", "Override address and port to listen on for HTTP(S) clients.")
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
{
|
||||
"listen": ":6060",
|
||||
"api_path": "/",
|
||||
"cache_control": 39600,
|
||||
"ws_compression_disabled": false,
|
||||
"static_mount": "/",
|
||||
"static_data": "static",
|
||||
"grpc_listen": ":16060",
|
||||
"grpc_keepalive_enabled": true,
|
||||
|
||||
"api_key_salt": "T713/rYYgW7g4m3vG6zGRh7+FM1t0T8j13koXScOAj4=",
|
||||
"max_message_size": 131072,
|
||||
"max_subscriber_count": 128,
|
||||
"max_tag_count": 16,
|
||||
"permanent_accounts": false,
|
||||
"expvar": "/debug/vars",
|
||||
"server_status": "-",
|
||||
"use_x_forwarded_for": true,
|
||||
"x_frame_options": "SAMEORIGIN",
|
||||
"default_country_code": "CN",
|
||||
"p2p_delete_enabled": true,
|
||||
"msg_delete_age": 600,
|
||||
"alias_tag": "alias",
|
||||
|
||||
"media": {
|
||||
"use_handler": "fs",
|
||||
"max_size": 104857600,
|
||||
"gc_period": 3600,
|
||||
"gc_block_size": 100,
|
||||
"handlers": {
|
||||
"fs": {
|
||||
"upload_dir": "uploads",
|
||||
"cache_control": "max-age=86400",
|
||||
"cors_origins": ["*"]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"webrtc": {
|
||||
"enabled": true,
|
||||
"call_establishment_timeout": 30,
|
||||
"ice_servers": [
|
||||
{
|
||||
"urls": ["stun:stun.l.google.com:19302"]
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
"tls": {
|
||||
"enabled": false,
|
||||
"http_redirect": "",
|
||||
"strict_max_age": 0
|
||||
},
|
||||
|
||||
"auth_config": {
|
||||
"logical_names": [],
|
||||
"basic": {
|
||||
"add_to_tags": false,
|
||||
"min_login_length": 1,
|
||||
"min_password_length": 6
|
||||
},
|
||||
"token": {
|
||||
"expire_in": 1209600,
|
||||
"serial_num": 1,
|
||||
"key": "wfaY2RgF2S1OQI/ZlK+LSrp1KB2jwAdGAIHQ7JZn+Kc="
|
||||
},
|
||||
"code": {
|
||||
"expire_in": 900,
|
||||
"max_retries": 3,
|
||||
"code_length": 6
|
||||
}
|
||||
},
|
||||
|
||||
"store_config": {
|
||||
"uid_key": "la6YsO+bNX/+XIkOqc5Svw==",
|
||||
"max_results": 1024,
|
||||
"use_adapter": "mysql",
|
||||
"adapters": {
|
||||
"mysql": {
|
||||
"dsn": "mysql://root:oWxz*2=sb),ouW%p@tcp(64.118.137.182:53306)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci",
|
||||
"tenant_registry_db": "hotgo",
|
||||
"tenant_db_routing": true,
|
||||
"max_open_conns": 64,
|
||||
"max_idle_conns": 64,
|
||||
"conn_max_lifetime": 60,
|
||||
"sql_timeout": 10
|
||||
}
|
||||
},
|
||||
"redis": {
|
||||
"addr": "64.118.137.182:56379",
|
||||
"username": "",
|
||||
"password": "HMTJzkm.h>Y+E9ep",
|
||||
"db": 0,
|
||||
"key_prefix": "im",
|
||||
"dial_timeout_ms": 2000,
|
||||
"read_timeout_ms": 2000,
|
||||
"write_timeout_ms": 2000,
|
||||
"pending_ttl_seconds": 120,
|
||||
"done_ttl_seconds": 86400,
|
||||
"failed_ttl_seconds": 600,
|
||||
"hot_message_ttl_seconds": 86400,
|
||||
"presence_ttl_seconds": 120,
|
||||
"cache_ttl_seconds": 300,
|
||||
"short_state_ttl_seconds": 600,
|
||||
"group_fanout_enabled": true
|
||||
}
|
||||
},
|
||||
|
||||
"kafka": {
|
||||
"enabled": true,
|
||||
"brokers": ["64.118.137.182:9094"],
|
||||
"client_id": "im-server",
|
||||
"consumer_group": "im-server",
|
||||
"dial_timeout_ms": 3000,
|
||||
"write_timeout_ms": 3000,
|
||||
"read_timeout_ms": 3000,
|
||||
"required_acks": 1,
|
||||
"allow_auto_topic_creation": false,
|
||||
"tls_enabled": false,
|
||||
"sasl_enabled": true,
|
||||
"sasl_mechanism": "SCRAM-SHA-512",
|
||||
"sasl_username": "kafka-external",
|
||||
"sasl_password": "gr58tNnlZOVGW3AmMbL2",
|
||||
"max_retry_count": 5,
|
||||
"retry_backoff_ms": [10000, 30000, 60000, 300000, 900000],
|
||||
"dead_letter_retention_seconds": 7776000,
|
||||
"topics": {
|
||||
"admin_command": "im.admin.command",
|
||||
"admin_command_result": "im.admin.command.result",
|
||||
"config_event": "im.config.event",
|
||||
"business_event": "im.business.event",
|
||||
"message_persist": "im.message.persist",
|
||||
"message_push": "im.message.push",
|
||||
"file_security": "im.file.security",
|
||||
"file_lifecycle": "im.file.lifecycle",
|
||||
"workbench_event": "im.workbench.event",
|
||||
"cache_invalidate": "im.cache.invalidate",
|
||||
"audit_event": "im.audit.event",
|
||||
"dead_letter": "im.dead.letter"
|
||||
}
|
||||
},
|
||||
"internal_api": {
|
||||
"enabled": true,
|
||||
"time_window_seconds": 300,
|
||||
"services": {
|
||||
"hotgo-admin": {
|
||||
"secret": "8569f5626a54950539eece9f731c9e3bf073daa74af8d23735d19afacac40f4b",
|
||||
"allow": ["admin_query"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"admin_internal_api": {
|
||||
"enabled": true,
|
||||
"base_url": "http://im-admin:8000",
|
||||
"service_name": "im-server",
|
||||
"secret": "b96832c04dbfb6dd768930ec9f082ecd90d87a1ffdaa493aa070617c48de9097",
|
||||
"timeout_ms": 3000
|
||||
},
|
||||
|
||||
"acc_validation": {
|
||||
"email": {
|
||||
"add_to_tags": false,
|
||||
"required": [],
|
||||
"config": {
|
||||
"host_url": "http://64.118.137.182:6060/",
|
||||
"sender": "MeetingIM <noreply@118im.com>",
|
||||
"validation_templ": "server/templ/email-validation-en.templ",
|
||||
"reset_secret_templ": "server/templ/email-password-reset-en.templ",
|
||||
"debug_response": "123456",
|
||||
"max_retries": 3
|
||||
}
|
||||
},
|
||||
"tel": {
|
||||
"add_to_tags": false,
|
||||
"required": [],
|
||||
"config": {
|
||||
"host_url": "http://64.118.137.182:6060/",
|
||||
"sender": "MeetingIM",
|
||||
"universal_templ": "server/templ/sms-universal-en.templ",
|
||||
"debug_response": "123456",
|
||||
"max_retries": 3
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
"acc_gc_config": {
|
||||
"enabled": true,
|
||||
"gc_period": 3600,
|
||||
"gc_block_size": 10,
|
||||
"gc_min_account_age": 30
|
||||
},
|
||||
|
||||
"push": [
|
||||
{
|
||||
"name": "stdout",
|
||||
"config": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "fcm",
|
||||
"config": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tnpg",
|
||||
"config": {
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
"cluster_config": {
|
||||
"self": "",
|
||||
"nodes": [],
|
||||
"failover": {
|
||||
"enabled": false
|
||||
}
|
||||
},
|
||||
|
||||
"plugins": []
|
||||
}
|
||||
@@ -9,7 +9,7 @@ HTTP_BASE_PORT=6060
|
||||
# Port where the first node will listen for gRPC intra-cluster connections.
|
||||
GRPC_BASE_PORT=16060
|
||||
|
||||
USAGE="Usage: $0 [ --config <path_to_tinode.conf> ] {start|stop}"
|
||||
USAGE="Usage: $0 [ --config <path_to_meetingim.conf> ] {start|stop}"
|
||||
|
||||
# Your server binary may have a different name and location.
|
||||
SERVER='./server'
|
||||
@@ -34,9 +34,9 @@ while [[ $# -gt 0 ]]; do
|
||||
;;
|
||||
start)
|
||||
if [ ! -z "$config" ] ; then
|
||||
TINODE_CONF=$config
|
||||
MEETINGIM_CONF=$config
|
||||
else
|
||||
TINODE_CONF="tinode.conf"
|
||||
MEETINGIM_CONF="meetingim.conf"
|
||||
fi
|
||||
if [ ! -z "${static_data+x}" ] ; then
|
||||
STATIC_DATA_DIR=$static_data
|
||||
@@ -51,10 +51,10 @@ while [[ $# -gt 0 ]]; do
|
||||
for NODE_NAME in "${ALL_NODE_NAMES[@]}"
|
||||
do
|
||||
# Start the node
|
||||
$SERVER -config=${TINODE_CONF} -cluster_self=${NODE_NAME} -listen=:${HTTP_PORT} -grpc_listen=:${GRPC_PORT} -static_data=${STATIC_DATA_DIR} -log_flags=stdFlags,shortfile &
|
||||
$SERVER -config=${MEETINGIM_CONF} -cluster_self=${NODE_NAME} -listen=:${HTTP_PORT} -grpc_listen=:${GRPC_PORT} -static_data=${STATIC_DATA_DIR} -log_flags=stdFlags,shortfile &
|
||||
# Save PID of the node to a temp file.
|
||||
# /var/tmp/ does not requre root access.
|
||||
echo $!> "/var/tmp/tinode-${NODE_NAME}.pid"
|
||||
echo $!> "/var/tmp/meetingim-${NODE_NAME}.pid"
|
||||
# Increment ports for the next node.
|
||||
HTTP_PORT=$((HTTP_PORT+1))
|
||||
GRPC_PORT=$((GRPC_PORT+1))
|
||||
@@ -67,9 +67,9 @@ while [[ $# -gt 0 ]]; do
|
||||
for NODE_NAME in "${ALL_NODE_NAMES[@]}"
|
||||
do
|
||||
# Read PIDs of running nodes from temp files and kill them.
|
||||
kill `cat /var/tmp/tinode-${NODE_NAME}.pid`
|
||||
kill `cat /var/tmp/meetingim-${NODE_NAME}.pid`
|
||||
# Clean up: delete temp files.
|
||||
rm "/var/tmp/tinode-${NODE_NAME}.pid"
|
||||
rm "/var/tmp/meetingim-${NODE_NAME}.pid"
|
||||
done
|
||||
exit 0
|
||||
;;
|
||||
|
||||
+12
-12
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
BINARY_PATH=$GOPATH/bin
|
||||
TINODE_BINARY=$BINARY_PATH/server
|
||||
MEETINGIM_BINARY=$BINARY_PATH/server
|
||||
|
||||
# Kills and removes any running containers.
|
||||
cleanup() {
|
||||
@@ -45,23 +45,23 @@ setup() {
|
||||
echo -n "."
|
||||
sleep 1
|
||||
done
|
||||
# Make sure there's no Tinode server binary in the current directory.
|
||||
# Make sure there's no MeetingIM server binary in the current directory.
|
||||
if [ -f "./server" ]; then
|
||||
rm ./server
|
||||
fi
|
||||
}
|
||||
|
||||
# Compiles Tinode binaries.
|
||||
# Compiles MeetingIM binaries.
|
||||
build() {
|
||||
go install -tags mysql -ldflags "-X main.buildstamp=`date -u '+%Y%m%dT%H:%M:%SZ'`" \
|
||||
github.com/tinode/chat/tinode-db \
|
||||
github.com/tinode/chat/server && \
|
||||
ln -s $TINODE_BINARY
|
||||
ln -s $MEETINGIM_BINARY
|
||||
}
|
||||
|
||||
# Initializes Tinode database.
|
||||
# Initializes MeetingIM database.
|
||||
init-db() {
|
||||
$GOPATH/bin/tinode-db -config=./tinode.conf -data=../tinode-db/data.json
|
||||
$GOPATH/bin/tinode-db -config=./meetingim.conf -data=../tinode-db/data.json
|
||||
}
|
||||
|
||||
wait-for() {
|
||||
@@ -71,7 +71,7 @@ wait-for() {
|
||||
done
|
||||
}
|
||||
|
||||
# Brings up a three-node Tinode cluster.
|
||||
# Brings up a three-node MeetingIM cluster.
|
||||
run-server() {
|
||||
./run-cluster.sh -s "" start && wait-for 16060
|
||||
}
|
||||
@@ -80,7 +80,7 @@ send-requests() {
|
||||
local expect=12
|
||||
local port=$1
|
||||
local id=$2
|
||||
local outfile=$(mktemp /tmp/tinode-${id}.txt)
|
||||
local outfile=$(mktemp /tmp/meetingim-${id}.txt)
|
||||
pushd .
|
||||
cd ../tn-cli
|
||||
python3 tn-cli.py --host=localhost:${port} --no-login < sample-script.txt > $outfile || fail "Test script failed (instance port ${port})"
|
||||
@@ -103,14 +103,14 @@ trap 'cleanup ; fail "For Unexpected Reasons"'\
|
||||
|
||||
run_id=`date +%s`
|
||||
echo "+----------------------------------------------------+"
|
||||
echo "| Tinode sanity test. |"
|
||||
echo "| MeetingIM sanity test. |"
|
||||
echo "+----------------------------------------------------+"
|
||||
echo "Timestamp = ${run_id}"
|
||||
|
||||
setup || fail "Test setup failed."
|
||||
build || fail "Could not build Tinode binaries"
|
||||
init-db || fail "Could not initialize Tinode database"
|
||||
run-server || fail "Could not start tinode"
|
||||
build || fail "Could not build MeetingIM binaries"
|
||||
init-db || fail "Could not initialize MeetingIM database"
|
||||
run-server || fail "Could not start meetingim"
|
||||
|
||||
# Test requests.
|
||||
send-requests 16060 $run_id
|
||||
|
||||
@@ -60,7 +60,7 @@ func openAdapter(workerId int, jsonconf json.RawMessage) error {
|
||||
adp = v
|
||||
}
|
||||
} else {
|
||||
return errors.New("store: db adapter is not specified. Please set `store_config.use_adapter` in `tinode.conf`")
|
||||
return errors.New("store: db adapter is not specified. Please set `store_config.use_adapter` in `meetingim.conf`")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
{{define "subject" -}}
|
||||
Reset Tinode password
|
||||
Reset MeetingIM password
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -17,7 +17,7 @@ Reset Tinode password
|
||||
|
||||
<p>Hello.</p>
|
||||
|
||||
<p>You recently requested to reset the password for your <a href="{{.HostUrl}}">Tinode</a> account.
|
||||
<p>You recently requested to reset the password for your <a href="{{.HostUrl}}">MeetingIM</a> account.
|
||||
Use the link or code below to reset it. The link and code are valid for the next 24 hours only.</p>
|
||||
|
||||
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&code={{.Code}}&cred={{.Cred}}">Click</a> to reset your password.</blockquote>
|
||||
@@ -36,7 +36,7 @@ Use the link or code below to reset it. The link and code are valid for the next
|
||||
|
||||
<p>If you did not request a password reset, please ignore this message.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Tinode Team</a></p>
|
||||
<p><a href="https://118im.com/">MeetingIM Team</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@ Use the link or code below to reset it. The link and code are valid for the next
|
||||
|
||||
Hello.
|
||||
|
||||
You recently requested to reset the password for your Tinode account ({{.HostUrl}}).
|
||||
You recently requested to reset the password for your MeetingIM account ({{.HostUrl}}).
|
||||
Use the link or code below to reset it. The link and code are valid for the next 24 hours only.
|
||||
|
||||
{{.HostUrl}}#reset?scheme={{.Scheme}}&code={{.Code}}&cred={{.Cred}}
|
||||
@@ -62,7 +62,7 @@ In case you have forgotten, here is your login: {{.}}.
|
||||
|
||||
If you did not request a password reset, please ignore this message.
|
||||
|
||||
Tinode Team
|
||||
https://tinode.co/
|
||||
MeetingIM Team
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
{{define "subject" -}}
|
||||
Reestablecer contraseña de Tinode
|
||||
Reestablecer contraseña de MeetingIM
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -17,7 +17,7 @@ Reestablecer contraseña de Tinode
|
||||
|
||||
<p>Hola.</p>
|
||||
|
||||
<p>Recientemente solicitaste reestablecer la contraseña para tu cuenta <a href="{{.HostUrl}}">Tinode</a>.
|
||||
<p>Recientemente solicitaste reestablecer la contraseña para tu cuenta <a href="{{.HostUrl}}">MeetingIM</a>.
|
||||
Usa el enlace de abajo para reestablecerla. El enlace es válido solamente por las siguientes 24 horas.</p>
|
||||
|
||||
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}">Clic</a> para reestablecer contraseña.</blockquote>
|
||||
@@ -36,7 +36,7 @@ Usa el enlace de abajo para reestablecerla. El enlace es válido solamente por l
|
||||
|
||||
<p>Si no solicitaste el reestablecimiento de tu contrseña, por favor ignora este mensaje.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Equipo de Tinode</a></p>
|
||||
<p><a href="https://118im.com/">Equipo de MeetingIM</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@ Usa el enlace de abajo para reestablecerla. El enlace es válido solamente por l
|
||||
|
||||
Hola.
|
||||
|
||||
Recientemente solicitaste reestablecer la contraseña para tu cuenta Tinode ({{.HostUrl}}).
|
||||
Recientemente solicitaste reestablecer la contraseña para tu cuenta MeetingIM ({{.HostUrl}}).
|
||||
Usa el enlace de abajo para reestablecerla. El enlace es válido solamente por las siguientes 24 horas.
|
||||
|
||||
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}
|
||||
@@ -62,7 +62,7 @@ En caso de que lo hayas olvidado, tu usuario es: {{.}}.
|
||||
|
||||
Si no solicitaste el reestablecimiento de tu contrseña, por favor ignora este mensaje.
|
||||
|
||||
Equipo de Tinode
|
||||
https://tinode.co/
|
||||
Equipo de MeetingIM
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
{{define "subject" -}}
|
||||
Réinitialiser votre mot de passe Tinode
|
||||
Réinitialiser votre mot de passe MeetingIM
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -17,7 +17,7 @@ Réinitialiser votre mot de passe Tinode
|
||||
|
||||
<p>Bonjour.</p>
|
||||
|
||||
<p>Vous avez récemment demandé à réinitialiser le mot de passe de votre compte <a href="{{.HostUrl}}">Tinode</a>.
|
||||
<p>Vous avez récemment demandé à réinitialiser le mot de passe de votre compte <a href="{{.HostUrl}}">MeetingIM</a>.
|
||||
Utilisez le lien ou le code ci-dessous pour le réinitialiser. Le lien et le code sont valides pour les prochaines 24 heures seulement.</p>
|
||||
|
||||
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}">Cliquer ici</a> pour réinitialiser votre mot de passe.</blockquote>
|
||||
@@ -36,7 +36,7 @@ Utilisez le lien ou le code ci-dessous pour le réinitialiser. Le lien et le cod
|
||||
|
||||
<p>Si vous n'avez pas demandé la réinitialisation de votre mot de passe, veuillez ignorer ce message.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">L'équipe Tinode</a></p>
|
||||
<p><a href="https://118im.com/">L'équipe MeetingIM</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -62,7 +62,7 @@ Au cas où vous l'auriez oublié, voici votre login : {{.}}.
|
||||
|
||||
Si vous n'avez pas demandé la réinitialisation de votre mot de passe, veuillez ignorer ce message.
|
||||
|
||||
L'équipe Tinode
|
||||
https://tinode.co/
|
||||
L'équipe MeetingIM
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
{{define "subject" -}}
|
||||
Redefinir senha Tinode
|
||||
Redefinir senha MeetingIM
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -17,7 +17,7 @@ Redefinir senha Tinode
|
||||
|
||||
<p>Olá.</p>
|
||||
|
||||
<p>Você solicitou recentemente a redefinição da sua senha <a href="{{.HostUrl}}">Tinode</a>.
|
||||
<p>Você solicitou recentemente a redefinição da sua senha <a href="{{.HostUrl}}">MeetingIM</a>.
|
||||
Use o link ou código abaixo para redefini-lo. O link e o código são válidos apenas pelas próximas 24 horas.</p>
|
||||
|
||||
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}">Clic</a> para redefinir sua senha.</blockquote>
|
||||
@@ -36,7 +36,7 @@ Use o link ou código abaixo para redefini-lo. O link e o código são válidos
|
||||
|
||||
<p>Se não solicitaste a redefinição da senha, por favor ignorar essa mensagem.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Equipe Tinode</a></p>
|
||||
<p><a href="https://118im.com/">Equipe MeetingIM</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@ Use o link ou código abaixo para redefini-lo. O link e o código são válidos
|
||||
|
||||
Olá.
|
||||
|
||||
Você solicitou recentemente a redefinição da sua senha Tinode ({{.HostUrl}}).
|
||||
Você solicitou recentemente a redefinição da sua senha MeetingIM ({{.HostUrl}}).
|
||||
Use o link ou código abaixo para redefini-lo. O link e o código são válidos apenas pelas próximas 24 horas.
|
||||
|
||||
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}
|
||||
@@ -62,7 +62,7 @@ Em caso de esquecimento, aqui seu login: {{.}}.
|
||||
|
||||
Se não solicitaste a redefinição da senha, por favor ignorar essa mensagem..
|
||||
|
||||
Equipe Tinode
|
||||
https://tinode.co/
|
||||
Equipe MeetingIM
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
{{define "subject" -}}
|
||||
Изменить пароль Tinode
|
||||
Изменить пароль MeetingIM
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<p>Здравствуйте.</p>
|
||||
|
||||
<p>Вы прислали запрос на изменение пароля для вашего аккаунта <a href="{{.HostUrl}}">Tinode</a>.
|
||||
<p>Вы прислали запрос на изменение пароля для вашего аккаунта <a href="{{.HostUrl}}">MeetingIM</a>.
|
||||
Используйте ссылку или код ниже, чтобы сбросить его. Ссылка и код действительны только в течение следующих 24 часов.</p>
|
||||
|
||||
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}&hl=RU">Кликните</a> для изменения пароля.</blockquote>
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<p>Если вы не отправляли запрос на изменение пароля, просто игнорируйте это сообщение.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Команда Tinode</a></p>
|
||||
<p><a href="https://118im.com/">Команда MeetingIM</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
Здравствуйте.
|
||||
|
||||
Вы прислали запрос на изменение пароля для вашего аккаунта Tinode ({{.HostUrl}}).
|
||||
Вы прислали запрос на изменение пароля для вашего аккаунта MeetingIM ({{.HostUrl}}).
|
||||
Используйте ссылку или код ниже, чтобы сбросить его. Ссылка и код действительны только в течение следующих 24 часов.
|
||||
|
||||
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}&hl=RU
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
Если вы не отправляли запрос на изменение пароля, просто игнорируйте это сообщение.
|
||||
|
||||
Команда Tinode
|
||||
https://tinode.co/
|
||||
Команда MeetingIM
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
{{define "subject" -}}
|
||||
Змінити пароль Tinode
|
||||
Змінити пароль MeetingIM
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<p>Доброго дня.</p>
|
||||
|
||||
<p>Ви надіслали запит на зміну пароля для вашого акаунта <a href="{{.HostUrl}}">Tinode</a>.
|
||||
<p>Ви надіслали запит на зміну пароля для вашого акаунта <a href="{{.HostUrl}}">MeetingIM</a>.
|
||||
Щоб скинути його, скористайтеся посиланням або кодом нижче. Посилання та код дійсні лише протягом наступних 24 годин.</p>
|
||||
|
||||
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}&hl=UK">Натисніть</a> для зміни пароля.</blockquote>
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<p>Якщо ви не надсилали запит на зміну пароля, просто ігноруйте це повідомлення.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Команда Tinode</a></p>
|
||||
<p><a href="https://118im.com/">Команда MeetingIM</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
Доброго дня.
|
||||
|
||||
Ви надіслали запит на зміну пароля для вашого акаунта Tinode ({{.HostUrl}}).
|
||||
Ви надіслали запит на зміну пароля для вашого акаунта MeetingIM ({{.HostUrl}}).
|
||||
Щоб скинути його, скористайтеся посиланням або кодом нижче. Посилання та код дійсні лише протягом наступних 24 годин.
|
||||
|
||||
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}&hl=UK
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
Якщо ви не надсилали запит на зміну пароля, просто ігноруйте це повідомлення.
|
||||
|
||||
Команда Tinode
|
||||
https://tinode.co/
|
||||
Команда MeetingIM
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
{{define "subject" -}}
|
||||
Đặt lại mật khẩu Tinode
|
||||
Đặt lại mật khẩu MeetingIM
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<p>Xin chào.</p>
|
||||
|
||||
<p>Bạn vừa gửi yêu cầu đặt lại mật khẩu cho tài khoản <a href="{{.HostUrl}}">Tinode</a> của bạn.
|
||||
<p>Bạn vừa gửi yêu cầu đặt lại mật khẩu cho tài khoản <a href="{{.HostUrl}}">MeetingIM</a> của bạn.
|
||||
Sử dụng liên kết hoặc mã dưới đây để thiết lập lại nó. Liên kết và mã chỉ có hiệu lực trong 24 giờ tới.</p>
|
||||
|
||||
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}">Bấm</a> để đặt lại mật khẩu.</blockquote>
|
||||
@@ -36,7 +36,7 @@ Sử dụng liên kết hoặc mã dưới đây để thiết lập lại nó.
|
||||
|
||||
<p>Nếu bạn không yêu cầu cấp lại mật khẩu, Vui lòng bỏ qua tin nhắn này.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Tinode Team</a></p>
|
||||
<p><a href="https://118im.com/">MeetingIM Team</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@ Sử dụng liên kết hoặc mã dưới đây để thiết lập lại nó.
|
||||
|
||||
Xin chào.
|
||||
|
||||
Bạn vừa gửi yêu cầu đặt lại mật khẩu cho tài khoản Tinode ({{.HostUrl}}).
|
||||
Bạn vừa gửi yêu cầu đặt lại mật khẩu cho tài khoản MeetingIM ({{.HostUrl}}).
|
||||
Sử dụng liên kết hoặc mã dưới đây để thiết lập lại nó. Liên kết và mã chỉ có hiệu lực trong 24 giờ tới.
|
||||
|
||||
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}
|
||||
@@ -62,7 +62,7 @@ Trong trường hợp bạn quên, thì đây là tên đăng nhập của bạn
|
||||
|
||||
Nếu bạn không yêu cầu cấp lại mật khẩu, Vui lòng bỏ qua tin nhắn này.
|
||||
|
||||
Tinode Team
|
||||
https://tinode.co/
|
||||
MeetingIM Team
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
{{define "subject" -}}
|
||||
重設 Tinode 密碼
|
||||
重設 MeetingIM 密碼
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<p>您好。</p>
|
||||
|
||||
<p>您最近請求重設您的 <a href="{{.HostUrl}}">Tinode</a> 帳戶密碼。
|
||||
<p>您最近請求重設您的 <a href="{{.HostUrl}}">MeetingIM</a> 帳戶密碼。
|
||||
請使用以下連結或驗證碼進行重設。此連結和驗證碼僅在接下來的 24 小時內有效。</p>
|
||||
|
||||
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&code={{.Code}}&cred={{.Cred}}">點擊</a>重設您的密碼。</blockquote>
|
||||
@@ -36,7 +36,7 @@
|
||||
|
||||
<p>如果您沒有請求密碼重設,請忽略此訊息。</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Tinode 團隊</a></p>
|
||||
<p><a href="https://118im.com/">MeetingIM 團隊</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@
|
||||
|
||||
您好。
|
||||
|
||||
您最近請求重設您的 Tinode 帳戶密碼 ({{.HostUrl}})。
|
||||
您最近請求重設您的 MeetingIM 帳戶密碼 ({{.HostUrl}})。
|
||||
請使用以下連結或驗證碼進行重設。此連結和驗證碼僅在接下來的 24 小時內有效。
|
||||
|
||||
{{.HostUrl}}#reset?scheme={{.Scheme}}&code={{.Code}}&cred={{.Cred}}
|
||||
@@ -62,7 +62,7 @@
|
||||
|
||||
如果您沒有請求密碼重設,請忽略此訊息。
|
||||
|
||||
Tinode 團隊
|
||||
https://tinode.co/
|
||||
MeetingIM 團隊
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
|
||||
{{define "subject" -}}
|
||||
重置 Tinode 密码
|
||||
重置 MeetingIM 密码
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
<p>您好!</p>
|
||||
|
||||
<p>您正在申请重置 <a href="{{.HostUrl}}">Tinode</a> 使用下面的链接或代码重置它。 链接和代码仅在接下来的 24 小时内有效。</p>
|
||||
<p>您正在申请重置 <a href="{{.HostUrl}}">MeetingIM</a> 使用下面的链接或代码重置它。 链接和代码仅在接下来的 24 小时内有效。</p>
|
||||
|
||||
<blockquote><a href="{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}">重置密码</a></blockquote>
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
<p>如非您没有申请重置密码,请忽略这条消息。</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Tinode 团队</a></p>
|
||||
<p><a href="https://118im.com/">MeetingIM 团队</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
您好!
|
||||
|
||||
您正在申请重置 Tinode ({{.HostUrl}}) 账号密码。
|
||||
您正在申请重置 MeetingIM ({{.HostUrl}}) 账号密码。
|
||||
使用下面的链接或代码重置它。 链接和代码仅在接下来的 24 小时内有效。
|
||||
|
||||
{{.HostUrl}}#reset?scheme={{.Scheme}}&token={{.Token}}
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
如非您没有申请重置密码,请忽略这条消息。
|
||||
|
||||
Tinode 团队
|
||||
https://tinode.co/
|
||||
MeetingIM 团队
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/}}
|
||||
|
||||
{{define "subject" -}}
|
||||
Tinode registration: confirm email
|
||||
MeetingIM registration: confirm email
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -24,7 +24,7 @@ Tinode registration: confirm email
|
||||
<p>Hello.</p>
|
||||
|
||||
<p>You're receiving this message because someone used your email to register at
|
||||
<a href="{{.HostUrl}}">Tinode</a>.</p>
|
||||
<a href="{{.HostUrl}}">MeetingIM</a>.</p>
|
||||
|
||||
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">Click to confirm</a>
|
||||
or go to
|
||||
@@ -33,9 +33,9 @@ and enter the following code:</p>
|
||||
<blockquote><big>{{.Code}}</big></blockquote>
|
||||
<p>You may need to enter login and password.</p>
|
||||
|
||||
<p>If you did not register at Tinode just ignore this message.</p>
|
||||
<p>If you did not register at MeetingIM just ignore this message.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Tinode Team</a></p>
|
||||
<p><a href="https://118im.com/">MeetingIM Team</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -45,7 +45,7 @@ and enter the following code:</p>
|
||||
|
||||
Hello.
|
||||
|
||||
You're receiving this message because someone used your email to register at Tinode ({{.HostUrl}}).
|
||||
You're receiving this message because someone used your email to register at MeetingIM ({{.HostUrl}}).
|
||||
|
||||
Click the link {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} to confirm or go to {{.HostUrl}}#cred?what=email
|
||||
and enter the following code:
|
||||
@@ -54,9 +54,9 @@ and enter the following code:
|
||||
|
||||
You may need to enter login and password.
|
||||
|
||||
If you did not register at Tinode just ignore this message.
|
||||
If you did not register at MeetingIM just ignore this message.
|
||||
|
||||
Tinode Team
|
||||
https://tinode.co/
|
||||
MeetingIM Team
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/}}
|
||||
|
||||
{{define "subject" -}}
|
||||
Registro Tinode: Correo de confirmación
|
||||
Registro MeetingIM: Correo de confirmación
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -15,7 +15,7 @@ Registro Tinode: Correo de confirmación
|
||||
<p>Hola.</p>
|
||||
|
||||
<p>Estás recibiendo este correo porque alguien uso tu correo para registrarse en
|
||||
<a href="{{.HostUrl}}">Tinode</a>.</p>
|
||||
<a href="{{.HostUrl}}">MeetingIM</a>.</p>
|
||||
|
||||
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">Clic para confirmar</a>
|
||||
o ve a
|
||||
@@ -24,9 +24,9 @@ e ingresa el siguiente código:</p>
|
||||
<blockquote><big>{{.Code}}</big></blockquote>
|
||||
<p>Necesitas ingresar tu usuario y contraseña.</p>
|
||||
|
||||
<p>Si tú no te registraste en Tinode solo ignora este mensaje.</p>
|
||||
<p>Si tú no te registraste en MeetingIM solo ignora este mensaje.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Equipo de Tinode</a></p>
|
||||
<p><a href="https://118im.com/">Equipo de MeetingIM</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,7 +36,7 @@ e ingresa el siguiente código:</p>
|
||||
|
||||
Hola.
|
||||
|
||||
Estás recibiendo este correo porque alguien uso tu correo para registrarse en Tinode ({{.HostUrl}}).
|
||||
Estás recibiendo este correo porque alguien uso tu correo para registrarse en MeetingIM ({{.HostUrl}}).
|
||||
|
||||
Da clic en el enlace {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} para confirmar o ve a {{.HostUrl}}#cred?what=email
|
||||
e ingresa el siguiente código:
|
||||
@@ -45,9 +45,9 @@ e ingresa el siguiente código:
|
||||
|
||||
Necesitas ingresar tu usuario y contraseña.
|
||||
|
||||
Si tú no te registraste en Tinode solo ignora este mensaje.
|
||||
Si tú no te registraste en MeetingIM solo ignora este mensaje.
|
||||
|
||||
Equipo de Tinode
|
||||
https://tinode.co/
|
||||
Equipo de MeetingIM
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/}}
|
||||
|
||||
{{define "subject" -}}
|
||||
Tinode enregistrement : confirmer l'adresse e-mail
|
||||
MeetingIM enregistrement : confirmer l'adresse e-mail
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -15,7 +15,7 @@ Tinode enregistrement : confirmer l'adresse e-mail
|
||||
<p>Bonjour.</p>
|
||||
|
||||
<p>Vous recevez ce message car quelqu'un a utilisé votre adresse électronique pour s'inscrire sur le site
|
||||
<a href="{{.HostUrl}}">Tinode</a>.</p>
|
||||
<a href="{{.HostUrl}}">MeetingIM</a>.</p>
|
||||
|
||||
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">Cliquer ici pour confimer</a>
|
||||
ou rendez-vous à l'adresse
|
||||
@@ -24,9 +24,9 @@ et entrez le code suivant :</p>
|
||||
<blockquote><big>{{.Code}}</big></blockquote>
|
||||
<p>Vous devrez peut-être entrer un login et un mot de passe.</p>
|
||||
|
||||
<p>Si vous ne vous êtes pas inscrit à Tinode, ignorez ce message.</p>
|
||||
<p>Si vous ne vous êtes pas inscrit à MeetingIM, ignorez ce message.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">L'équipe Tinode</a></p>
|
||||
<p><a href="https://118im.com/">L'équipe MeetingIM</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,7 +36,7 @@ et entrez le code suivant :</p>
|
||||
|
||||
Bonjour.
|
||||
|
||||
Vous recevez ce message car quelqu'un a utilisé votre adresse électronique pour s'inscrire sur le site Tinode ({{.HostUrl}}).
|
||||
Vous recevez ce message car quelqu'un a utilisé votre adresse électronique pour s'inscrire sur le site MeetingIM ({{.HostUrl}}).
|
||||
|
||||
Cliquer sur le lien {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} pour confimer ou rendez-vous à l'adresse {{.HostUrl}}#cred?what=email
|
||||
et entrez le code suivant :
|
||||
@@ -45,9 +45,9 @@ et entrez le code suivant :
|
||||
|
||||
Vous devrez peut-être entrer un login et un mot de passe.
|
||||
|
||||
Si vous ne vous êtes pas inscrit à Tinode, ignorez ce message.
|
||||
Si vous ne vous êtes pas inscrit à MeetingIM, ignorez ce message.
|
||||
|
||||
L'équipe Tinode
|
||||
https://tinode.co/
|
||||
L'équipe MeetingIM
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/}}
|
||||
|
||||
{{define "subject" -}}
|
||||
Registro Tinode: E-mail de confirmação
|
||||
Registro MeetingIM: E-mail de confirmação
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -15,7 +15,7 @@ Registro Tinode: E-mail de confirmação
|
||||
<p>Olá.</p>
|
||||
|
||||
<p> Você está recebendo esse e-mail porque alguém usou seu e-mail para registrar-se em
|
||||
<a href="{{.HostUrl}}">Tinode</a>.</p>
|
||||
<a href="{{.HostUrl}}">MeetingIM</a>.</p>
|
||||
|
||||
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">Clique para confirmar</a>
|
||||
ou acesse
|
||||
@@ -24,9 +24,9 @@ e entre com o seguinte código:</p>
|
||||
<blockquote><big>{{.Code}}</big></blockquote>
|
||||
<p>Necessário entrar com login e senha.</p>
|
||||
|
||||
<p>Se você não se registrou em Tinode apenas ignore essa mensagem.</p>
|
||||
<p>Se você não se registrou em MeetingIM apenas ignore essa mensagem.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Equipe Tinode</a></p>
|
||||
<p><a href="https://118im.com/">Equipe MeetingIM</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,7 +36,7 @@ e entre com o seguinte código:</p>
|
||||
|
||||
Olá.
|
||||
|
||||
Você está recebendo este e-mail porque alguém usou seu e-mail para registrar-se em Tinode ({{.HostUrl}}).
|
||||
Você está recebendo este e-mail porque alguém usou seu e-mail para registrar-se em MeetingIM ({{.HostUrl}}).
|
||||
|
||||
Clique no link {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} para confirmar {{.HostUrl}}#cred?what=email
|
||||
e entre com o seguinte código:
|
||||
@@ -45,9 +45,9 @@ e entre com o seguinte código:
|
||||
|
||||
Necessário entrar com login e senha.
|
||||
|
||||
Se você não se registrou em Tinode apenas ignore essa mensagem..
|
||||
Se você não se registrou em MeetingIM apenas ignore essa mensagem..
|
||||
|
||||
Equipe Tinode
|
||||
https://tinode.co/
|
||||
Equipe MeetingIM
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/}}
|
||||
|
||||
{{define "subject" -}}
|
||||
Регистрация Tinode: подтвердите емейл
|
||||
Регистрация MeetingIM: подтвердите емейл
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<p>Здравствуйте.</p>
|
||||
|
||||
<p>Вы получили это сообщение потому, что зарегистрировались в <a href="{{.HostUrl}}">Tinode</a>.</p>
|
||||
<p>Вы получили это сообщение потому, что зарегистрировались в <a href="{{.HostUrl}}">MeetingIM</a>.</p>
|
||||
|
||||
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}&hl=RU">Кликните здесь чтобы подтвердить</a>
|
||||
регистрацию или перейдите по сслыке
|
||||
@@ -23,9 +23,9 @@
|
||||
<blockquote><big>{{.Code}}</big></blockquote>
|
||||
<p>Возможно, вам потребуется ввести логин и пароль.</p>
|
||||
|
||||
<p>Если вы не регистрировались в Tinode, просто игнорируйте это сообщение.</p>
|
||||
<p>Если вы не регистрировались в MeetingIM, просто игнорируйте это сообщение.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Команда Tinode</a></p>
|
||||
<p><a href="https://118im.com/">Команда MeetingIM</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
Здравствуйте.
|
||||
|
||||
Вы получили это сообщение потому, что зарегистрировались в Tinode ({{.HostUrl}}).
|
||||
Вы получили это сообщение потому, что зарегистрировались в MeetingIM ({{.HostUrl}}).
|
||||
|
||||
Кликните на {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}&hl=RU чтобы подтвердить
|
||||
регистрацию или перейдите по сслыке {{.HostUrl}}#cred?what=email">{{.HostUrl}}#cred?method=email&hl=RU
|
||||
@@ -45,9 +45,9 @@
|
||||
|
||||
Возможно, вам также потребуется ввести логин и пароль.
|
||||
|
||||
Если вы не регистрировались в Tinode, просто игнорируйте это сообщение.
|
||||
Если вы не регистрировались в MeetingIM, просто игнорируйте это сообщение.
|
||||
|
||||
Команда Tinode
|
||||
https://tinode.co/
|
||||
Команда MeetingIM
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/}}
|
||||
|
||||
{{define "subject" -}}
|
||||
Реєстрація Tinode: підтвердіть емейл
|
||||
Реєстрація MeetingIM: підтвердіть емейл
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -14,7 +14,7 @@
|
||||
|
||||
<p>Доброго дня.</p>
|
||||
|
||||
<p>Ви отримали це повідомлення тому, що зареєструвалися в <a href="{{.HostUrl}}">Tinode</a>.</p>
|
||||
<p>Ви отримали це повідомлення тому, що зареєструвалися в <a href="{{.HostUrl}}">MeetingIM</a>.</p>
|
||||
|
||||
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}&hl=UK">Натисніть тут, щоб підтвердити</a>
|
||||
реєстрацію або перейдіть за посиланням
|
||||
@@ -23,9 +23,9 @@
|
||||
<blockquote><big>{{.Code}}</big></blockquote>
|
||||
<p>Можливо, вам потрібно буде ввести логін та пароль.</p>
|
||||
|
||||
<p>Якщо ви не реєструвалися в Tinode, просто ігноруйте це повідомлення.</p>
|
||||
<p>Якщо ви не реєструвалися в MeetingIM, просто ігноруйте це повідомлення.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Команда Tinode</a></p>
|
||||
<p><a href="https://118im.com/">Команда MeetingIM</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -35,7 +35,7 @@
|
||||
|
||||
Доброго дня.
|
||||
|
||||
Ви отримали це повідомлення тому, що зареєструвалися в Tinode ({{.HostUrl}}).
|
||||
Ви отримали це повідомлення тому, що зареєструвалися в MeetingIM ({{.HostUrl}}).
|
||||
|
||||
Натисніть на {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}&hl=UK щоб підтвердити
|
||||
реєстрацію або перейдіть за посиланням {{.HostUrl}}#cred?what=email">{{.HostUrl}}#cred?method=email&hl=UK
|
||||
@@ -45,9 +45,9 @@
|
||||
|
||||
Можливо, вам потрібно буде ввести логін та пароль.
|
||||
|
||||
Якщо ви не реєструвалися в Tinode, просто ігноруйте це повідомлення.
|
||||
Якщо ви не реєструвалися в MeetingIM, просто ігноруйте це повідомлення.
|
||||
|
||||
Команда Tinode
|
||||
https://tinode.co/
|
||||
Команда MeetingIM
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/}}
|
||||
|
||||
{{define "subject" -}}
|
||||
Xác thực đăng ký tài khoản Tinode
|
||||
Xác thực đăng ký tài khoản MeetingIM
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -15,7 +15,7 @@ Xác thực đăng ký tài khoản Tinode
|
||||
<p>Xin chào.</p>
|
||||
|
||||
<p>Bạn nhận được tin nhắn này bởi vì có ai đó đã dùng email này để đăng ký tài khoản tại
|
||||
<a href="{{.HostUrl}}">Tinode</a>.</p>
|
||||
<a href="{{.HostUrl}}">MeetingIM</a>.</p>
|
||||
|
||||
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">Bấm để xác nhận</a>
|
||||
hoặc đi tới liên kết
|
||||
@@ -24,9 +24,9 @@ và nhập mã xác thực:</p>
|
||||
<blockquote><big>{{.Code}}</big></blockquote>
|
||||
<p>Có thể bạn cần nhập tên đăng nhập và mật khẩu.</p>
|
||||
|
||||
<p>Nếu bạn không đăng ký tài khoản tại Tinode vui lòng bỏ qua tin nhắn này.</p>
|
||||
<p>Nếu bạn không đăng ký tài khoản tại MeetingIM vui lòng bỏ qua tin nhắn này.</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Tinode Team</a></p>
|
||||
<p><a href="https://118im.com/">MeetingIM Team</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -36,7 +36,7 @@ và nhập mã xác thực:</p>
|
||||
|
||||
Xin chào.
|
||||
|
||||
Bạn nhận được tin nhắn này bởi vì có ai đó đã dùng email này để đăng ký tài khoản tại Tinode ({{.HostUrl}}).
|
||||
Bạn nhận được tin nhắn này bởi vì có ai đó đã dùng email này để đăng ký tài khoản tại MeetingIM ({{.HostUrl}}).
|
||||
|
||||
Bấm vào liên kết {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} để xác nhận hoặc đi tới {{.HostUrl}}#cred?what=email
|
||||
và nhập mã xác thực
|
||||
@@ -45,9 +45,9 @@ và nhập mã xác thực
|
||||
|
||||
Có thể bạn sẽ cần nhập tên đăng nhập và mật khẩu.
|
||||
|
||||
Nếu bạn không đăng ký tài khoản tại Tinode vui lòng bỏ qua tin nhắn này.
|
||||
Nếu bạn không đăng ký tài khoản tại MeetingIM vui lòng bỏ qua tin nhắn này.
|
||||
|
||||
Tinode Team
|
||||
https://tinode.co/
|
||||
MeetingIM Team
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/}}
|
||||
|
||||
{{define "subject" -}}
|
||||
Tinode 註冊:確認電子郵件
|
||||
MeetingIM 註冊:確認電子郵件
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -25,7 +25,7 @@ Tinode 註冊:確認電子郵件
|
||||
<p>您好。</p>
|
||||
|
||||
<p>您收到此訊息是因為有人使用您的電子郵件在
|
||||
<a href="{{.HostUrl}}">Tinode</a> 註冊。</p>
|
||||
<a href="{{.HostUrl}}">MeetingIM</a> 註冊。</p>
|
||||
|
||||
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">點擊確認</a>
|
||||
或前往
|
||||
@@ -34,9 +34,9 @@ Tinode 註冊:確認電子郵件
|
||||
<blockquote><big>{{.Code}}</big></blockquote>
|
||||
<p>您可能需要輸入登入名稱和密碼。</p>
|
||||
|
||||
<p>如果您沒有在 Tinode 註冊,請忽略此訊息。</p>
|
||||
<p>如果您沒有在 MeetingIM 註冊,請忽略此訊息。</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Tinode 團隊</a></p>
|
||||
<p><a href="https://118im.com/">MeetingIM 團隊</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -46,7 +46,7 @@ Tinode 註冊:確認電子郵件
|
||||
|
||||
您好。
|
||||
|
||||
您收到此訊息是因為有人使用您的電子郵件在 Tinode ({{.HostUrl}}) 註冊。
|
||||
您收到此訊息是因為有人使用您的電子郵件在 MeetingIM ({{.HostUrl}}) 註冊。
|
||||
|
||||
點擊連結 {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} 進行確認,或前往 {{.HostUrl}}#cred?what=email
|
||||
並輸入以下驗證碼:
|
||||
@@ -55,9 +55,9 @@ Tinode 註冊:確認電子郵件
|
||||
|
||||
您可能需要輸入登入名稱和密碼。
|
||||
|
||||
如果您沒有在 Tinode 註冊,請忽略此訊息。
|
||||
如果您沒有在 MeetingIM 註冊,請忽略此訊息。
|
||||
|
||||
Tinode 團隊
|
||||
https://tinode.co/
|
||||
MeetingIM 團隊
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
*/}}
|
||||
|
||||
{{define "subject" -}}
|
||||
Tinode 注册: 确认邮件
|
||||
MeetingIM 注册: 确认邮件
|
||||
{{- end}}
|
||||
|
||||
{{define "body_html" -}}
|
||||
@@ -23,7 +23,7 @@ Tinode 注册: 确认邮件
|
||||
|
||||
<p>您好!</p>
|
||||
|
||||
<p>您收到此消息是因为您注册了<a href="{{.HostUrl}}">Tinode</a>。</p>
|
||||
<p>您收到此消息是因为您注册了<a href="{{.HostUrl}}">MeetingIM</a>。</p>
|
||||
|
||||
<p><a href="{{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}}">确认注册</a>
|
||||
或者跳转至链接
|
||||
@@ -32,9 +32,9 @@ Tinode 注册: 确认邮件
|
||||
<blockquote><big>{{.Code}}</big></blockquote>
|
||||
<p>您可能需要输入登录名和密码。</p>
|
||||
|
||||
<p>如果您没有注册Tinode,请忽略这条消息。</p>
|
||||
<p>如果您没有注册MeetingIM,请忽略这条消息。</p>
|
||||
|
||||
<p><a href="https://tinode.co/">Tinode 团队</a></p>
|
||||
<p><a href="https://118im.com/">MeetingIM 团队</a></p>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -44,7 +44,7 @@ Tinode 注册: 确认邮件
|
||||
|
||||
您好!
|
||||
|
||||
您收到此消息是因为您注册了 Tinode ({{.HostUrl}})。
|
||||
您收到此消息是因为您注册了 MeetingIM ({{.HostUrl}})。
|
||||
|
||||
点击链接 {{.HostUrl}}#cred?method=email&code={{.Code}}&token={{.Token}} 确认注册或者跳转至链接 {{.HostUrl}}#cred?what=email
|
||||
并输入验证码:
|
||||
@@ -53,9 +53,9 @@ Tinode 注册: 确认邮件
|
||||
|
||||
您可能需要输入登录名和密码。
|
||||
|
||||
如果您没有注册Tinode,请忽略这条消息。
|
||||
如果您没有注册MeetingIM,请忽略这条消息。
|
||||
|
||||
Tinode 团队
|
||||
https://tinode.co/
|
||||
MeetingIM 团队
|
||||
https://118im.com/
|
||||
|
||||
{{- end}}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
Universal confirmation and password reset template for SMS.
|
||||
*/}}
|
||||
|
||||
Tinode confirmation code: {{.Code}}
|
||||
MeetingIM confirmation code: {{.Code}}
|
||||
{{.HostUrl}}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
Universal confirmation and password reset template for SMS.
|
||||
*/}}
|
||||
|
||||
Código de confirmación de Tinode: {{.Code}}
|
||||
Código de confirmación de MeetingIM: {{.Code}}
|
||||
{{.HostUrl}}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
Modèle universel de confirmation et de réinitialisation du mot de passe pour SMS.
|
||||
*/}}
|
||||
|
||||
Code de confirmation Tinode : {{.Code}}
|
||||
Code de confirmation MeetingIM : {{.Code}}
|
||||
{{.HostUrl}}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
Modelo universal de confirmação e redefinição de senha para SMS.
|
||||
*/}}
|
||||
|
||||
Código de confirmação Tinode: {{.Code}}
|
||||
Código de confirmação MeetingIM: {{.Code}}
|
||||
{{.HostUrl}}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
Универсальный шаблон подтверждения и сброса пароля для СМС.
|
||||
*/}}
|
||||
|
||||
Код подтверждения Tinode: {{.Code}}
|
||||
Код подтверждения MeetingIM: {{.Code}}
|
||||
{{.HostUrl}}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
Універсальний шаблон підтвердження та скидання пароля для СМС.
|
||||
*/}}
|
||||
|
||||
Код підтвердження Tinode: {{.Code}}
|
||||
Код підтвердження MeetingIM: {{.Code}}
|
||||
{{.HostUrl}}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
Universal confirmation and password reset template for SMS.
|
||||
*/}}
|
||||
|
||||
Mã xác thực từ Tinode: {{.Code}}
|
||||
Mã xác thực từ MeetingIM: {{.Code}}
|
||||
{{.HostUrl}}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
通用確認和密碼重設簡訊模板。
|
||||
*/}}
|
||||
|
||||
Tinode 確認驗證碼:{{.Code}}
|
||||
MeetingIM 確認驗證碼:{{.Code}}
|
||||
{{.HostUrl}}
|
||||
|
||||
@@ -4,5 +4,5 @@
|
||||
Universal confirmation and password reset template for SMS.
|
||||
*/}}
|
||||
|
||||
【Tinode】验证码: {{.Code}}
|
||||
【MeetingIM】验证码: {{.Code}}
|
||||
{{.HostUrl}}
|
||||
|
||||
+2
-2
@@ -163,7 +163,7 @@
|
||||
"required": [],
|
||||
"config": {
|
||||
"host_url": "http://64.118.137.182:6060/",
|
||||
"sender": "Tinode <noreply@118im.com>",
|
||||
"sender": "MeetingIM <noreply@118im.com>",
|
||||
"validation_templ": "server/templ/email-validation-en.templ",
|
||||
"reset_secret_templ": "server/templ/email-password-reset-en.templ",
|
||||
"debug_response": "123456",
|
||||
@@ -175,7 +175,7 @@
|
||||
"required": [],
|
||||
"config": {
|
||||
"host_url": "http://64.118.137.182:6060/",
|
||||
"sender": "Tinode",
|
||||
"sender": "MeetingIM",
|
||||
"universal_templ": "server/templ/sms-universal-en.templ",
|
||||
"debug_response": "123456",
|
||||
"max_retries": 3
|
||||
|
||||
@@ -124,7 +124,7 @@ func TestServeIMPasswordResetVerify(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/password-reset/challenges/challenge-1/verify",
|
||||
bytes.NewBufferString(`{"code":"123456"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
@@ -197,7 +197,7 @@ func TestServeIMPasswordResetConfirm(t *testing.T) {
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/v1/im/auth/password-reset/confirm",
|
||||
bytes.NewBufferString(`{"reset_token":"challenge-1.`+token+`","new_password":"new-password"}`))
|
||||
req.Header.Set("X-Tinode-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-MeetingIM-APIKey", makeTestAPIKey(t))
|
||||
req.Header.Set("X-Tenant-Code", "acme")
|
||||
rec := httptest.NewRecorder()
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const (
|
||||
// Default code length when one is not provided in the config
|
||||
defaultCodeLength = 6
|
||||
|
||||
defaultSender = "Tinode"
|
||||
defaultSender = "MeetingIM"
|
||||
)
|
||||
|
||||
func (v *validator) Init(jsonconf string) error {
|
||||
|
||||
+2
-2
@@ -415,8 +415,8 @@ def upload(id, cmd, args):
|
||||
result = requests.post(
|
||||
scheme + '://' + args.web_host + '/v' + PROTOCOL_VERSION + '/file/u/',
|
||||
headers = {
|
||||
'X-Tinode-APIKey': args.api_key,
|
||||
'X-Tinode-Auth': 'Token ' + tn_globals.AuthToken,
|
||||
'X-MeetingIM-APIKey': args.api_key,
|
||||
'X-MeetingIM-Auth': 'Token ' + tn_globals.AuthToken,
|
||||
'User-Agent': APP_NAME + " " + APP_VERSION + "/" + LIB_VERSION
|
||||
},
|
||||
data = {'id': id},
|
||||
|
||||
Reference in New Issue
Block a user