fix: align IM client interaction edge cases

This commit is contained in:
richjohnrambo
2026-09-13 10:06:52 +08:00
parent e5fbca735d
commit d107fba8bf
5 changed files with 137 additions and 21 deletions
+86 -12
View File
@@ -45,6 +45,7 @@ const (
imReasonRealNameRequired = "REALNAME_REQUIRED"
imReasonActionToken = "ACTION_TOKEN_INVALID"
imReasonPermissionDenied = "PERMISSION_DENIED"
imReasonForbidden = "FORBIDDEN"
imReasonRiskConfirmation = "RISK_CONFIRMATION_REQUIRED"
imReasonSchemaUnsupported = "SCHEMA_UNSUPPORTED"
imReasonIdempotencyConflict = "IDEMPOTENCY_CONFLICT"
@@ -587,6 +588,7 @@ type imSystemMessageAction struct {
Type string `json:"type"`
Label string `json:"label,omitempty"`
TargetID string `json:"target_id,omitempty"`
MinRole string `json:"min_role,omitempty"`
}
func serveIMTenantResolve(wrt http.ResponseWriter, req *http.Request) {
@@ -4430,15 +4432,7 @@ func serveIMConversationContext(wrt http.ResponseWriter, req *http.Request) {
return
}
if tail == "messages" {
if req.Method != http.MethodGet {
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
return
}
if topic == "" {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
return
}
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": []any{}, "next_cursor": "", "has_more": false})
serveIMConversationMessages(wrt, req, principal, topic)
return
}
if tail != "context" {
@@ -4466,6 +4460,52 @@ func serveIMConversationContext(wrt http.ResponseWriter, req *http.Request) {
writeIMSuccess(wrt, req, relationshipContextPayload(ctx))
}
func serveIMConversationMessages(wrt http.ResponseWriter, req *http.Request, principal *imPrincipal, topic string) {
if topic == "" {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
return
}
topic = normalizeConversationSettingsTopic(topic, principal.Uid)
switch req.Method {
case http.MethodGet:
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": []any{}, "next_cursor": "", "has_more": false})
case http.MethodDelete:
sub, err := store.Subs.Get(principal.TenantID, topic, principal.Uid, false)
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
if sub == nil || !(sub.ModeGiven&sub.ModeWant).IsReader() {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonForbidden, "permission denied", nil)
return
}
top, err := store.Topics.Get(principal.TenantID, topic)
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
if top == nil {
writeIMError(wrt, req, http.StatusNotFound, 30004, imReasonResourceNotFound, "conversation not found", nil)
return
}
delID := top.DelId + 1
if delID <= 0 {
delID = int(time.Now().Unix())
}
var ranges []types.Range
if top.SeqId > 0 {
ranges = []types.Range{{Low: 1, Hi: top.SeqId + 1}}
}
if err = store.Messages.DeleteList(principal.TenantID, topic, delID, principal.Uid, 0, ranges); err != nil {
writeIMRelationError(wrt, req, err)
return
}
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "cleared": true, "deleted_id": delID})
default:
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
}
}
func serveIMConversationSettings(wrt http.ResponseWriter, req *http.Request, principal *imPrincipal, topic string) {
if topic == "" {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
@@ -4904,7 +4944,7 @@ func serveIMGroupResource(wrt http.ResponseWriter, req *http.Request) {
serveIMGroupOwnerTransfer(wrt, req, topic)
return
}
if tail == "mutes" || strings.HasPrefix(tail, "mutes/") {
if tail == "mutes" || tail == "mutes/batch" || strings.HasPrefix(tail, "mutes/") {
serveIMGroupMutes(wrt, req, topic, tail)
return
}
@@ -5928,9 +5968,24 @@ func serveIMGroupGovernance(wrt http.ResponseWriter, req *http.Request, topic st
[]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}},
[]imSystemMessageAction{{Type: "open_group_invite_settings", Label: "群聊邀请确认", TargetID: topic, MinRole: "admin"}},
map[string]any{"join_approval": *body.JoinApproval, "version": settings.Version})
}
if body.MemberPrivateChat != nil && imReliableHTTPEventsEnabled(req) {
template := "group_member_private_chat_disabled"
text := "群成员私信权限已关闭,群成员头像和名片不可点击查看。"
if *body.MemberPrivateChat {
template = "group_member_private_chat_enabled"
text = "群成员私信权限已开启,群成员头像和名片可点击查看。"
}
publishIMConversationSystemNotice(principal.TenantID, topic,
"system_notice", template, text,
[]imSystemMessageActor{
{UserID: principal.Uid.UserId(), Click: map[string]any{"type": "user_profile", "target_id": principal.Uid.UserId()}},
},
nil,
map[string]any{"member_private_chat": *body.MemberPrivateChat, "version": settings.Version})
}
dispatchIMGroupEvents(principal.TenantID)
writeIMSuccess(wrt, req, groupGovernancePayloadFromSettings(settings, selfSub))
default:
@@ -6086,7 +6141,7 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
return
}
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": groupMutePayloads(items), "next_cursor": nextCursor})
case tail == "mutes" && req.Method == http.MethodPost:
case (tail == "mutes" || tail == "mutes/batch") && req.Method == http.MethodPost:
var body imGroupMuteBatchRequest
if !decodeIMJSONBody(wrt, req, &body) {
return
@@ -6411,6 +6466,10 @@ func serveIMUserProfile(wrt http.ResponseWriter, req *http.Request, identifier s
return
}
if !imPrivacyAllowsProfile(privacy, ctx.Relationship) {
if ctx.Relationship == types.RelationshipBlockedByMe {
writeIMSuccess(wrt, req, blockedUserProfilePayload(user, ctx))
return
}
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "profile is private", nil)
return
}
@@ -9749,6 +9808,21 @@ func userProfilePayload(user *types.User, ctx *types.RelationshipContext) map[st
return data
}
func blockedUserProfilePayload(user *types.User, ctx *types.RelationshipContext) map[string]any {
data := userSummaryPayload(user, ctx.Remark)
data["relationship"] = ctx.Relationship
data["topic"] = ctx.Topic
data["capabilities"] = map[string]any{
"can_send": false,
"can_call": false,
"can_add_friend": false,
}
if ctx.Remark != "" {
data["remark"] = ctx.Remark
}
return data
}
func copyOptionalProfileFields(data map[string]any, public map[string]any) {
for _, key := range []string{"nickname", "gender", "region", "public_account", "signature"} {
if value, ok := public[key]; ok {