feat: add conversation system action messages
This commit is contained in:
+33
-1
@@ -314,6 +314,38 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
|||||||
| @所有人 | `{"pub":{"id":"pub-all-1","topic":"grp...","head":{"mime":"application/json","x-im-schema":1,"x-im-type":"text","x-im-client-mid":"mid_all_1","mention_all":true},"content":{"text":"@所有人 开会"}}}` | 无权限时 `{"ctrl":{"id":"pub-all-1","code":403,"text":"permission denied","params":{"reason":"MENTION_ALL_FORBIDDEN"}}}` |
|
| @所有人 | `{"pub":{"id":"pub-all-1","topic":"grp...","head":{"mime":"application/json","x-im-schema":1,"x-im-type":"text","x-im-client-mid":"mid_all_1","mention_all":true},"content":{"text":"@所有人 开会"}}}` | 无权限时 `{"ctrl":{"id":"pub-all-1","code":403,"text":"permission denied","params":{"reason":"MENTION_ALL_FORBIDDEN"}}}` |
|
||||||
| 重复 `client_mid` 重试 | 与第一次发送完全相同,复用 `x-im-client-mid:"mid_1"` | 目标行为:返回第一次成功的同一 `seq`;不得落两条消息 |
|
| 重复 `client_mid` 重试 | 与第一次发送完全相同,复用 `x-im-client-mid:"mid_1"` | 目标行为:返回第一次成功的同一 `seq`;不得落两条消息 |
|
||||||
|
|
||||||
|
##### 0.5.4.3.1 会话内系统提示和可点击动作消息
|
||||||
|
|
||||||
|
截图中的灰色提示、入群邀请说明、好友已添加提示等属于会话内系统消息,必须进入对应 P2P 或群 Topic 的消息流,占用 `seq`,参与历史拉取、重连补偿和多端同步。它们不是 `info` 实时业务事件,也不是系统通知列表记录;`info` 只负责触发刷新,真正需要在聊天窗口长期展示的内容必须以 `{data}` 下发并入库。
|
||||||
|
|
||||||
|
客户端不得主动发送 `system_notice`、`system_action`;这两类消息只能由服务端受控身份生成。服务端下发时 `data.from` 可以为空,客户端以 `head.x-im-from="system"` 判断系统身份,不依赖真实用户头像。客户端收到未知 `template` 或未知 `action.type` 时,展示 `content.text`,忽略不可识别动作,不允许执行任意 URL、脚本或非白名单路由。
|
||||||
|
|
||||||
|
| 类型 | 适用场景 | 服务端下行示例 | 客户端渲染 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `system_notice` | “你已添加了安妮,现在可以开始聊天了”、撤回提示、群名修改、成员进出群等纯提示 | `{"data":{"topic":"p2p...","seq":19,"from":"","head":{"x-im-schema":1,"x-im-type":"system_notice","x-im-template":"friend_added","x-im-from":"system"},"content":{"text":"你已添加了安妮,现在可以开始聊天了。","level":"normal","template":"friend_added","actors":[{"user_id":"usr_anne","name":"安妮"}],"actions":[]}}}` | 居中灰色提示;不显示发送人头像;如 `actors.click` 为空则不可点 |
|
||||||
|
| `system_action` | 群邀请确认说明、审批入口、群设置入口、可点击人名或群名提示 | `{"data":{"topic":"grp...","seq":20,"from":"","head":{"x-im-schema":1,"x-im-type":"system_action","x-im-template":"group_invite_confirm_enabled","x-im-from":"system"},"content":{"text":"安妮邀请栗子来了加入了群聊","template":"group_invite_confirm_enabled","actors":[{"user_id":"usr_anne","name":"安妮","click":{"type":"user_profile","target_id":"usr_anne"}},{"user_id":"usr_lizi","name":"栗子来了","click":{"type":"user_profile","target_id":"usr_lizi"}}],"actions":[{"type":"open_group_invite_settings","label":"群聊邀请确认","target_id":"grp..."}]}}}` | 居中灰色或业务提示样式;可点击片段按 `actors.click` 高亮;按钮或文字入口按 `actions` 渲染 |
|
||||||
|
|
||||||
|
`content` 字段约定:
|
||||||
|
|
||||||
|
| 字段 | 类型 | 必填 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `text` | string | 是 | 降级展示文案;客户端可直接展示,也可按 `template` 重新排版 |
|
||||||
|
| `template` | string | 是 | 服务端稳定模板编码,例如 `friend_added`、`group_member_joined`、`group_invite_confirm_enabled` |
|
||||||
|
| `level` | string | 否 | `normal`、`warning`、`danger`,影响颜色和强调程度 |
|
||||||
|
| `actors` | array | 否 | 文案中的用户、群、文件等实体;支持局部高亮和点击 |
|
||||||
|
| `actions` | array | 否 | 当前消息可触发的操作入口;只允许客户端白名单动作 |
|
||||||
|
| `extra` | object | 否 | 模板扩展参数;客户端不得依赖未知字段完成关键流程 |
|
||||||
|
|
||||||
|
`actors[].click` 和 `actions[]` 的 `type` 首版白名单:
|
||||||
|
|
||||||
|
| 类型 | 参数 | 行为 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `user_profile` | `target_id` | 打开用户资料页 |
|
||||||
|
| `group_profile` | `target_id` | 打开群资料页 |
|
||||||
|
| `open_group_invite_settings` | `target_id` | 打开群邀请确认设置 |
|
||||||
|
| `open_group_join_requests` | `target_id` | 打开群入群审批列表 |
|
||||||
|
| `open_message` | `topic,seq` | 定位到指定消息 |
|
||||||
|
|
||||||
##### 0.5.4.4 回执、正在输入、删除和清空记录
|
##### 0.5.4.4 回执、正在输入、删除和清空记录
|
||||||
|
|
||||||
| 场景 | 客户端发送 | 服务端返回/下行示例 |
|
| 场景 | 客户端发送 | 服务端返回/下行示例 |
|
||||||
@@ -453,7 +485,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
|
|||||||
|
|
||||||
`x-im-client-mid` 由客户端生成 ULID/UUID,作用域为“租户 + 发送人”,断线重试必须复用;服务端目标实现应返回第一次写入的同一 `seq`。未知 `x-im-type` 显示“暂不支持的消息”,不得崩溃或删除消息。
|
`x-im-client-mid` 由客户端生成 ULID/UUID,作用域为“租户 + 发送人”,断线重试必须复用;服务端目标实现应返回第一次写入的同一 `seq`。未知 `x-im-type` 显示“暂不支持的消息”,不得崩溃或删除消息。
|
||||||
|
|
||||||
消息类型首版固定为:`text`、`image`、`video`、`voice`、`file`、`contact_card`、`link`、`forward_bundle`、`call`、`system_event`。自定义表情启用后追加 `sticker`。媒体内容只保存 `file_id`、文件元数据和缩略图 ID,不保存可长期绕过鉴权的公共 URL。
|
消息类型首版固定为:`text`、`image`、`video`、`voice`、`file`、`contact_card`、`link`、`forward_bundle`、`call`、`system_notice`、`system_action`、`system_event`。自定义表情启用后追加 `sticker`。媒体内容只保存 `file_id`、文件元数据和缩略图 ID,不保存可长期绕过鉴权的公共 URL。`system_notice` 和 `system_action` 只能由服务端生成,普通用户构造后发送必须被服务端拒绝。
|
||||||
|
|
||||||
### 0.7 HTTP 幂等、重试和安全
|
### 0.7 HTTP 幂等、重试和安全
|
||||||
|
|
||||||
|
|||||||
@@ -8635,8 +8635,9 @@ func (a *adapter) TenantFriendshipList(id t.TenantID, uid t.Uid, queryText, init
|
|||||||
if limit <= 0 || limit > 100 {
|
if limit <= 0 || limit > 100 {
|
||||||
limit = 50
|
limit = 50
|
||||||
}
|
}
|
||||||
where := []string{"fs.user_id=?", "fs.status=?"}
|
uidID := decodeUID(uid)
|
||||||
args := []any{decodeUID(uid), t.RelationshipFriend}
|
where := []string{"fs.user_id=?", "fs.status=?", "fs.peer_id<>?"}
|
||||||
|
args := []any{uidID, t.RelationshipFriend, uidID}
|
||||||
if cursor != "" {
|
if cursor != "" {
|
||||||
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
parsed, err := strconv.ParseInt(cursor, 10, 64)
|
||||||
if err != nil || parsed <= 0 {
|
if err != nil || parsed <= 0 {
|
||||||
|
|||||||
@@ -551,6 +551,18 @@ type imGroupMuteBatchRequest struct {
|
|||||||
Reason string `json:"reason,omitempty"`
|
Reason string `json:"reason,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type imSystemMessageActor struct {
|
||||||
|
UserID string `json:"user_id,omitempty"`
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
Click map[string]any `json:"click,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type imSystemMessageAction struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Label string `json:"label,omitempty"`
|
||||||
|
TargetID string `json:"target_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
func serveIMTenantResolve(wrt http.ResponseWriter, req *http.Request) {
|
func serveIMTenantResolve(wrt http.ResponseWriter, req *http.Request) {
|
||||||
if req.Method != http.MethodPost {
|
if req.Method != http.MethodPost {
|
||||||
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
|
||||||
@@ -4169,6 +4181,9 @@ func serveIMFriends(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
}
|
}
|
||||||
payload := make([]map[string]any, 0, len(items))
|
payload := make([]map[string]any, 0, len(items))
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
|
if item.Friendship.PeerID == principal.Uid {
|
||||||
|
continue
|
||||||
|
}
|
||||||
payload = append(payload, friendListItemPayload(item))
|
payload = append(payload, friendListItemPayload(item))
|
||||||
}
|
}
|
||||||
writeIMSuccess(wrt, req, map[string]any{"items": payload, "next_cursor": nextCursor})
|
writeIMSuccess(wrt, req, map[string]any{"items": payload, "next_cursor": nextCursor})
|
||||||
@@ -4934,6 +4949,16 @@ func serveIMGroupInvitationResource(wrt http.ResponseWriter, req *http.Request)
|
|||||||
writeIMRelationError(wrt, req, err)
|
writeIMRelationError(wrt, req, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if imReliableHTTPEventsEnabled(req) {
|
||||||
|
publishIMConversationSystemNotice(principal.TenantID, inv.Topic,
|
||||||
|
"system_action", "group_member_joined", "有成员通过邀请加入了群聊。",
|
||||||
|
[]imSystemMessageActor{
|
||||||
|
{UserID: inv.InviterUserID.UserId(), Click: map[string]any{"type": "user_profile", "target_id": inv.InviterUserID.UserId()}},
|
||||||
|
{UserID: inv.TargetUserID.UserId(), Click: map[string]any{"type": "user_profile", "target_id": inv.TargetUserID.UserId()}},
|
||||||
|
},
|
||||||
|
[]imSystemMessageAction{{Type: "group_profile", Label: "查看群聊", TargetID: inv.Topic}},
|
||||||
|
map[string]any{"invitation_id": inv.InvitationID, "source": "invitation"})
|
||||||
|
}
|
||||||
dispatchIMGroupEvents(principal.TenantID)
|
dispatchIMGroupEvents(principal.TenantID)
|
||||||
writeIMSuccess(wrt, req, groupInvitationPayload(inv))
|
writeIMSuccess(wrt, req, groupInvitationPayload(inv))
|
||||||
}
|
}
|
||||||
@@ -5048,6 +5073,16 @@ func serveIMGroupJoinRequestAction(wrt http.ResponseWriter, req *http.Request, t
|
|||||||
writeIMRelationError(wrt, req, err)
|
writeIMRelationError(wrt, req, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if updated.Status == types.GroupJoinRequestStatusAccepted && imReliableHTTPEventsEnabled(req) {
|
||||||
|
publishIMConversationSystemNotice(principal.TenantID, topic,
|
||||||
|
"system_action", "group_member_joined", "有成员通过审批加入了群聊。",
|
||||||
|
[]imSystemMessageActor{
|
||||||
|
{UserID: updated.ApplicantUserID.UserId(), Click: map[string]any{"type": "user_profile", "target_id": updated.ApplicantUserID.UserId()}},
|
||||||
|
{UserID: principal.Uid.UserId(), Click: map[string]any{"type": "user_profile", "target_id": principal.Uid.UserId()}},
|
||||||
|
},
|
||||||
|
[]imSystemMessageAction{{Type: "group_profile", Label: "查看群聊", TargetID: topic}},
|
||||||
|
map[string]any{"request_id": updated.RequestID, "source": "approval"})
|
||||||
|
}
|
||||||
dispatchIMGroupEvents(principal.TenantID)
|
dispatchIMGroupEvents(principal.TenantID)
|
||||||
writeIMSuccess(wrt, req, groupJoinRequestPayload(updated))
|
writeIMSuccess(wrt, req, groupJoinRequestPayload(updated))
|
||||||
}
|
}
|
||||||
@@ -5176,6 +5211,21 @@ func serveIMGroupGovernance(wrt http.ResponseWriter, req *http.Request, topic st
|
|||||||
writeIMRelationError(wrt, req, err)
|
writeIMRelationError(wrt, req, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if body.JoinApproval != nil && imReliableHTTPEventsEnabled(req) {
|
||||||
|
template := "group_invite_confirm_disabled"
|
||||||
|
text := "群聊邀请确认已关闭,成员邀请好友可直接进群。"
|
||||||
|
if *body.JoinApproval {
|
||||||
|
template = "group_invite_confirm_enabled"
|
||||||
|
text = "群聊邀请确认已启用,群成员邀请好友进群需要群主或管理员确认。"
|
||||||
|
}
|
||||||
|
publishIMConversationSystemNotice(principal.TenantID, topic,
|
||||||
|
"system_action", template, text,
|
||||||
|
[]imSystemMessageActor{
|
||||||
|
{UserID: principal.Uid.UserId(), Click: map[string]any{"type": "user_profile", "target_id": principal.Uid.UserId()}},
|
||||||
|
},
|
||||||
|
[]imSystemMessageAction{{Type: "open_group_invite_settings", Label: "群聊邀请确认", TargetID: topic}},
|
||||||
|
map[string]any{"join_approval": *body.JoinApproval, "version": settings.Version})
|
||||||
|
}
|
||||||
dispatchIMGroupEvents(principal.TenantID)
|
dispatchIMGroupEvents(principal.TenantID)
|
||||||
writeIMSuccess(wrt, req, groupGovernancePayloadFromSettings(settings, selfSub))
|
writeIMSuccess(wrt, req, groupGovernancePayloadFromSettings(settings, selfSub))
|
||||||
default:
|
default:
|
||||||
@@ -5794,6 +5844,14 @@ func serveIMFriendRequestAction(wrt http.ResponseWriter, req *http.Request) {
|
|||||||
writeIMRelationError(wrt, req, err)
|
writeIMRelationError(wrt, req, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if status == types.FriendRequestAccepted && imReliableHTTPEventsEnabled(req) {
|
||||||
|
publishIMConversationSystemNotice(principal.TenantID, updated.ApplicantID.P2PName(updated.TargetID),
|
||||||
|
"system_notice", "friend_added", "你们已成为好友,现在可以开始聊天了。",
|
||||||
|
[]imSystemMessageActor{
|
||||||
|
{UserID: updated.ApplicantID.UserId(), Click: map[string]any{"type": "user_profile", "target_id": updated.ApplicantID.UserId()}},
|
||||||
|
{UserID: updated.TargetID.UserId(), Click: map[string]any{"type": "user_profile", "target_id": updated.TargetID.UserId()}},
|
||||||
|
}, nil, map[string]any{"request_id": updated.RequestID})
|
||||||
|
}
|
||||||
dispatchIMBusinessEvents(principal.TenantID)
|
dispatchIMBusinessEvents(principal.TenantID)
|
||||||
if imReliableHTTPEventsEnabled(req) {
|
if imReliableHTTPEventsEnabled(req) {
|
||||||
eventType := "friend.request." + status
|
eventType := "friend.request." + status
|
||||||
@@ -9248,6 +9306,64 @@ func requireIMTopicReadable(tenantID types.TenantID, uid types.Uid, topic string
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func publishIMConversationSystemNotice(tenantID types.TenantID, topic, msgType, template, text string, actors []imSystemMessageActor, actions []imSystemMessageAction, extra map[string]any) {
|
||||||
|
if tenantID == types.ZeroTenantID || strings.TrimSpace(topic) == "" || store.Store == nil || !store.Store.IsOpen() || store.Topics == nil || store.Messages == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msgType = strings.TrimSpace(msgType)
|
||||||
|
if msgType == "" {
|
||||||
|
msgType = "system_notice"
|
||||||
|
}
|
||||||
|
if msgType != "system_notice" && msgType != "system_action" {
|
||||||
|
logs.Warn.Printf("im system message: unsupported type %q for topic %s", msgType, topic)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
template = strings.TrimSpace(template)
|
||||||
|
text = strings.TrimSpace(text)
|
||||||
|
if template == "" || text == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
topicRecord, err := store.Topics.Get(tenantID, topic)
|
||||||
|
if err != nil || topicRecord == nil {
|
||||||
|
if err != nil {
|
||||||
|
logs.Warn.Printf("im system message: failed to load topic %s: %v", topic, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content := map[string]any{
|
||||||
|
"text": text,
|
||||||
|
"template": template,
|
||||||
|
"level": "normal",
|
||||||
|
}
|
||||||
|
if len(actors) > 0 {
|
||||||
|
content["actors"] = actors
|
||||||
|
}
|
||||||
|
if len(actions) > 0 {
|
||||||
|
content["actions"] = actions
|
||||||
|
}
|
||||||
|
if len(extra) > 0 {
|
||||||
|
content["extra"] = extra
|
||||||
|
}
|
||||||
|
now := time.Now().UTC().Round(time.Millisecond)
|
||||||
|
_, err = store.Messages.Save(tenantID, &types.Message{
|
||||||
|
ObjHeader: types.ObjHeader{CreatedAt: now, UpdatedAt: now},
|
||||||
|
TenantID: tenantID,
|
||||||
|
SeqId: topicRecord.SeqId + 1,
|
||||||
|
Topic: topic,
|
||||||
|
From: types.ZeroUid.String(),
|
||||||
|
Head: types.KVMap{
|
||||||
|
"x-im-schema": 1,
|
||||||
|
"x-im-type": msgType,
|
||||||
|
"x-im-template": template,
|
||||||
|
"x-im-from": "system",
|
||||||
|
},
|
||||||
|
Content: content,
|
||||||
|
}, nil, false)
|
||||||
|
if err != nil && !errors.Is(err, types.ErrDuplicate) {
|
||||||
|
logs.Warn.Printf("im system message: failed to save %s for topic %s: %v", template, topic, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func systemNotificationIDFromPath(path string) string {
|
func systemNotificationIDFromPath(path string) string {
|
||||||
const marker = "/v1/im/system-notifications/"
|
const marker = "/v1/im/system-notifications/"
|
||||||
idx := strings.Index(path, marker)
|
idx := strings.Index(path, marker)
|
||||||
|
|||||||
@@ -5172,6 +5172,15 @@ func TestServeIMFriendsList(t *testing.T) {
|
|||||||
now := time.Now().UTC().Round(time.Millisecond)
|
now := time.Now().UTC().Round(time.Millisecond)
|
||||||
peer := types.Uid(2)
|
peer := types.Uid(2)
|
||||||
relations := &testRelationsStore{friends: []types.FriendListItem{{
|
relations := &testRelationsStore{friends: []types.FriendListItem{{
|
||||||
|
Friendship: types.Friendship{
|
||||||
|
TenantID: 7,
|
||||||
|
UserID: 1,
|
||||||
|
PeerID: 1,
|
||||||
|
Status: types.RelationshipFriend,
|
||||||
|
Topic: types.Uid(1).P2PName(types.Uid(1)),
|
||||||
|
},
|
||||||
|
Peer: &types.User{ObjHeader: types.ObjHeader{Id: types.Uid(1).String()}, TenantID: 7, Public: map[string]any{"display_name": "Self"}},
|
||||||
|
}, {
|
||||||
Friendship: types.Friendship{
|
Friendship: types.Friendship{
|
||||||
TenantID: 7,
|
TenantID: 7,
|
||||||
UserID: 1,
|
UserID: 1,
|
||||||
|
|||||||
@@ -3570,7 +3570,7 @@ func validIMMessageHeaderToken(value string, max int) bool {
|
|||||||
func validIMMessageType(value string) bool {
|
func validIMMessageType(value string) bool {
|
||||||
switch strings.TrimSpace(value) {
|
switch strings.TrimSpace(value) {
|
||||||
case "text", "image", "file", "audio", "voice", "video", "location", "contact", "contact_card",
|
case "text", "image", "file", "audio", "voice", "video", "location", "contact", "contact_card",
|
||||||
"card", "link", "forward_bundle", "sticker", "call", "system", "system_event":
|
"card", "link", "forward_bundle", "sticker", "call", "system", "system_notice", "system_action", "system_event":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ func TestValidateMessageHeadersProductContract(t *testing.T) {
|
|||||||
{"x-im-schema": json.Number("1"), "x-im-type": "voice"},
|
{"x-im-schema": json.Number("1"), "x-im-type": "voice"},
|
||||||
{"x-im-schema": "1", "x-im-type": "contact_card"},
|
{"x-im-schema": "1", "x-im-type": "contact_card"},
|
||||||
{"x-im-schema": 1, "type": "audio", "x-im-type": "voice"},
|
{"x-im-schema": 1, "type": "audio", "x-im-type": "voice"},
|
||||||
|
{"x-im-schema": 1, "x-im-type": "system_notice"},
|
||||||
|
{"x-im-schema": 1, "x-im-type": "system_action"},
|
||||||
{"type": "system"},
|
{"type": "system"},
|
||||||
}
|
}
|
||||||
for _, head := range valid {
|
for _, head := range valid {
|
||||||
|
|||||||
+10
-2
@@ -1060,8 +1060,7 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
|
|||||||
if msg.sess != nil && msg.sess.authLvl != auth.LevelRoot {
|
if msg.sess != nil && msg.sess.authLvl != auth.LevelRoot {
|
||||||
legacyType, _ := head["type"].(string)
|
legacyType, _ := head["type"].(string)
|
||||||
productType, _ := head["x-im-type"].(string)
|
productType, _ := head["x-im-type"].(string)
|
||||||
if legacyType == "system" || legacyType == "system_event" ||
|
if isSystemGeneratedMessageType(legacyType) || isSystemGeneratedMessageType(productType) {
|
||||||
productType == "system" || productType == "system_event" {
|
|
||||||
msg.sess.queueOut(ErrPermissionDenied(msg.Id, t.original(asUid), msg.Timestamp))
|
msg.sess.queueOut(ErrPermissionDenied(msg.Id, t.original(asUid), msg.Timestamp))
|
||||||
return types.ErrPermissionDenied
|
return types.ErrPermissionDenied
|
||||||
}
|
}
|
||||||
@@ -1190,6 +1189,15 @@ func (t *Topic) saveAndBroadcastMessage(msg *ClientComMessage, asUid types.Uid,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isSystemGeneratedMessageType(value string) bool {
|
||||||
|
switch strings.TrimSpace(value) {
|
||||||
|
case "system", "system_notice", "system_action", "system_event":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// handlePubBroadcast fans out {pub} -> {data} messages to recipients in a master topic.
|
// handlePubBroadcast fans out {pub} -> {data} messages to recipients in a master topic.
|
||||||
// This is a NON-proxy broadcast.
|
// This is a NON-proxy broadcast.
|
||||||
func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
|
func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
|
||||||
|
|||||||
@@ -789,6 +789,8 @@ func TestHandleBroadcastDataDuplicateClientMessage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleBroadcastDataRejectsUserSystemType(t *testing.T) {
|
func TestHandleBroadcastDataRejectsUserSystemType(t *testing.T) {
|
||||||
|
for _, msgType := range []string{"system_event", "system_notice", "system_action"} {
|
||||||
|
t.Run(msgType, func(t *testing.T) {
|
||||||
numUsers := 2
|
numUsers := 2
|
||||||
helper := TopicTestHelper{}
|
helper := TopicTestHelper{}
|
||||||
helper.setUp(t, numUsers, types.TopicCatP2P, "p2p-test", true)
|
helper.setUp(t, numUsers, types.TopicCatP2P, "p2p-test", true)
|
||||||
@@ -800,7 +802,7 @@ func TestHandleBroadcastDataRejectsUserSystemType(t *testing.T) {
|
|||||||
AsUser: from,
|
AsUser: from,
|
||||||
Pub: &MsgClientPub{
|
Pub: &MsgClientPub{
|
||||||
Topic: "p2p",
|
Topic: "p2p",
|
||||||
Head: map[string]any{"x-im-schema": float64(1), "x-im-type": "system_event"},
|
Head: map[string]any{"x-im-schema": float64(1), "x-im-type": msgType},
|
||||||
Content: "test",
|
Content: "test",
|
||||||
},
|
},
|
||||||
sess: helper.sessions[0],
|
sess: helper.sessions[0],
|
||||||
@@ -828,6 +830,8 @@ func TestHandleBroadcastDataRejectsUserSystemType(t *testing.T) {
|
|||||||
if len(helper.hubMessages) != 0 {
|
if len(helper.hubMessages) != 0 {
|
||||||
t.Errorf("Hubhelper.route did not expect any messages, however %d received.", len(helper.hubMessages))
|
t.Errorf("Hubhelper.route did not expect any messages, however %d received.", len(helper.hubMessages))
|
||||||
}
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleBroadcastDataRejectsMentionAllByMember(t *testing.T) {
|
func TestHandleBroadcastDataRejectsMentionAllByMember(t *testing.T) {
|
||||||
|
|||||||
Reference in New Issue
Block a user