fix: repair group management edge cases

This commit is contained in:
richjohnrambo
2026-09-13 16:04:31 +08:00
parent a9afc6ff6a
commit 7b304412b0
3 changed files with 473 additions and 184 deletions
+147 -38
View File
@@ -122,11 +122,6 @@ func withIMHTTPProtocol(prefix string, next http.Handler) http.Handler {
return
}
req = req.WithContext(context.WithValue(req.Context(), imHTTPProtocolValidated, true))
if req.Method == http.MethodGet || req.Method == http.MethodHead {
if principal, err := resolveIMHTTPBearer(req); err == nil {
dispatchIMBusinessEvents(principal.TenantID)
}
}
serveIMHTTPIdempotent(wrt, req, next)
})
}
@@ -187,7 +182,6 @@ func serveIMHTTPIdempotent(wrt http.ResponseWriter, req *http.Request, next http
next.ServeHTTP(wrt, req)
return
}
dispatchIMBusinessEvents(principal.TenantID)
key := strings.TrimSpace(req.Header.Get("Idempotency-Key"))
if key == "" || len(key) > 128 {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "Idempotency-Key is required", map[string]any{"field": "Idempotency-Key"})
@@ -514,6 +508,7 @@ type imGroupPatchRequest struct {
type imGroupMemberSettingsRequest struct {
Pinned *bool `json:"pinned,omitempty"`
MuteNotifications *bool `json:"mute_notifications,omitempty"`
Version int `json:"version,omitempty"`
}
type imGroupMemberProfileUpdateRequest struct {
@@ -570,12 +565,18 @@ type imGroupOwnerTransferRequest struct {
Version int `json:"version,omitempty"`
}
type imGroupDissolveRequest struct {
Reason string `json:"reason,omitempty"`
}
type imGroupMuteBatchRequest struct {
Members []struct {
UserID string `json:"user_id"`
ExpiresAt string `json:"expires_at,omitempty"`
} `json:"members"`
Reason string `json:"reason,omitempty"`
UserIDs []string `json:"user_ids,omitempty"`
Reason string `json:"reason,omitempty"`
Version int `json:"version,omitempty"`
}
type imSystemMessageActor struct {
@@ -1248,6 +1249,7 @@ func serveIMPasswordResetConfirm(wrt http.ResponseWriter, req *http.Request) {
if globals.sessionStore != nil {
globals.sessionStore.EvictUser(vc.TargetUser, "")
}
publishRealtimeEvictUser(tenant.ID, vc.TargetUser, "password_reset", "")
writeIMSuccess(wrt, req, map[string]any{"status": "ok"})
}
@@ -4320,10 +4322,7 @@ func serveIMMessageRecall(wrt http.ResponseWriter, req *http.Request) {
}
topic = normalizeConversationSettingsTopic(topic, principal.Uid)
var body imMessageRecallRequest
if !decodeIMJSONBody(wrt, req, &body) || body.Version <= 0 {
if body.Version <= 0 {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "version"})
}
if !decodeIMJSONBody(wrt, req, &body) {
return
}
msg, authorized, err := authorizeIMMessageMutation(principal, topic, seq, true)
@@ -4339,8 +4338,12 @@ func serveIMMessageRecall(wrt http.ResponseWriter, req *http.Request) {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "message recall window expired", nil)
return
}
version := body.Version
if version <= 0 {
version = msg.Version
}
recalledAt := time.Now().UTC().Round(time.Millisecond)
updated, err := store.Messages.Recall(principal.TenantID, topic, seq, principal.Uid, body.Version, recalledAt)
updated, err := store.Messages.Recall(principal.TenantID, topic, seq, principal.Uid, version, recalledAt)
if err != nil {
writeIMMessageMutationError(wrt, req, err)
return
@@ -4683,7 +4686,6 @@ func serveIMFriendRequestCreate(wrt http.ResponseWriter, req *http.Request) {
writeIMRelationError(wrt, req, err)
return
}
dispatchIMBusinessEvents(principal.TenantID)
if imReliableHTTPEventsEnabled(req) {
if err = publishIMRelationEvent(principal.TenantID, principal.Uid, target, created.RequestID,
"friend.request.created", "新的好友申请", message); err != nil {
@@ -4864,7 +4866,8 @@ func serveIMGroupCreate(wrt http.ResponseWriter, req *http.Request) {
writeIMRelationError(wrt, req, err)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topicName, principal.Uid, "group.created",
map[string]any{"topic": topicName, "member_user_ids": userIDs(memberIDs)})
created, err := store.Topics.Get(principal.TenantID, topicName)
if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
@@ -4888,7 +4891,7 @@ func serveIMGroupResource(wrt http.ResponseWriter, req *http.Request) {
serveIMGroupSettings(wrt, req, topic)
return
}
if tail == "member-settings" {
if tail == "member-settings" || tail == "members/me/settings" {
serveIMGroupMemberSettings(wrt, req, topic)
return
}
@@ -5098,6 +5101,9 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
}
public := groupPublicMap(group.Public)
currentVersion := intFromAny(public["version"])
if settings, err := store.GroupBusiness.GetSettings(principal.TenantID, topic); err == nil && settings != nil && settings.Version > 0 {
currentVersion = settings.Version
}
if currentVersion <= 0 {
currentVersion = 1
}
@@ -5151,7 +5157,8 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
return
}
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.settings.updated",
map[string]any{"topic": topic})
updated, err := store.Topics.Get(principal.TenantID, topic)
if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
@@ -5164,6 +5171,33 @@ func serveIMGroupDetail(wrt http.ResponseWriter, req *http.Request, topic string
}
}
writeIMSuccess(wrt, req, payload)
case http.MethodDelete:
if !(selfSub.ModeGiven & selfSub.ModeWant).IsOwner() {
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
return
}
if !consumeIMActionTokenHeader(wrt, req, principal, "group_dissolve") {
return
}
var body imGroupDissolveRequest
if req.Body != nil && req.Body != http.NoBody {
if !decodeIMJSONBody(wrt, req, &body) {
return
}
}
settings, err := store.GroupBusiness.Dissolve(principal.TenantID, topic, principal.Uid, strings.TrimSpace(body.Reason), time.Now().UTC().Round(time.Millisecond))
if err != nil {
writeIMRelationError(wrt, req, err)
return
}
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.dissolved",
map[string]any{"topic": topic})
payload := map[string]any{"topic": topic, "deleted": true}
if settings != nil {
payload["status"] = settings.GroupStatus
payload["version"] = settings.Version
}
writeIMSuccess(wrt, req, payload)
default:
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
}
@@ -5205,13 +5239,21 @@ func serveIMGroupMemberSettings(wrt http.ResponseWriter, req *http.Request, topi
return
}
private := groupPrivateMap(selfSub.Private)
currentVersion := intFromAny(private["version"])
if currentVersion <= 0 {
currentVersion = 1
}
if body.Version > 0 && body.Version != currentVersion {
writeIMError(wrt, req, http.StatusConflict, 30006, "VERSION_CONFLICT", "version conflict", map[string]any{"current_version": currentVersion})
return
}
if body.Pinned != nil {
private["pinned"] = *body.Pinned
}
if body.MuteNotifications != nil {
private["mute_notifications"] = *body.MuteNotifications
}
private["version"] = intFromAny(private["version"]) + 1
private["version"] = currentVersion + 1
private["updated_at"] = time.Now().UTC().Round(time.Millisecond).Format(time.RFC3339Nano)
if err := store.Subs.Update(principal.TenantID, topic, principal.Uid, map[string]any{"Private": private}); err != nil {
writeIMRelationError(wrt, req, err)
@@ -5224,7 +5266,8 @@ func serveIMGroupMemberSettings(wrt http.ResponseWriter, req *http.Request, topi
writeIMRelationError(wrt, req, err)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.updated",
map[string]any{"user_id": principal.Uid.UserId()})
selfSub.Private = private
writeIMSuccess(wrt, req, groupMemberSettingsPayload(selfSub))
}
@@ -5275,7 +5318,8 @@ func serveIMGroupMyProfile(wrt http.ResponseWriter, req *http.Request, topic str
if _, err := store.GroupBusiness.UpdateMember(principal.TenantID, topic, principal.Uid, types.GroupMemberPatch{Nickname: &nickname}); err != nil {
if errors.Is(err, types.ErrNotFound) {
logs.Warn.Printf("group[%s]: member profile saved to subscription only; business member row missing for user %s", topic, principal.Uid.UserId())
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.updated",
map[string]any{"user_id": principal.Uid.UserId()})
writeIMSuccess(wrt, req, map[string]any{"nickname": nickname, "version": currentVersion + 1, "updated_at": updatedAt.Format(time.RFC3339Nano)})
return
}
@@ -5285,7 +5329,8 @@ func serveIMGroupMyProfile(wrt http.ResponseWriter, req *http.Request, topic str
writeIMRelationError(wrt, req, err)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.updated",
map[string]any{"user_id": principal.Uid.UserId()})
writeIMSuccess(wrt, req, map[string]any{"nickname": nickname, "version": currentVersion + 1, "updated_at": updatedAt.Format(time.RFC3339Nano)})
}
@@ -5306,7 +5351,8 @@ func serveIMGroupLeave(wrt http.ResponseWriter, req *http.Request, topic string)
writeIMRelationError(wrt, req, err)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.left",
map[string]any{"user_id": principal.Uid.UserId()})
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "status": "left"})
}
@@ -5466,7 +5512,8 @@ func serveIMGroupInvitations(wrt http.ResponseWriter, req *http.Request, topic s
publishIMGroupInvitationNotification(principal.TenantID, inv)
results = append(results, item)
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.invitations.updated",
map[string]any{"topic": topic, "items": results})
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": results})
}
@@ -5630,7 +5677,8 @@ func serveIMGroupInvitationResource(wrt http.ResponseWriter, req *http.Request)
[]imSystemMessageAction{{Type: "group_profile", Label: "查看群聊", TargetID: inv.Topic}},
map[string]any{"invitation_id": inv.InvitationID, "source": "invitation"})
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, inv.Topic, principal.Uid, "group.member.joined",
map[string]any{"user_id": principal.Uid.UserId(), "invitation_id": inv.InvitationID})
writeIMSuccess(wrt, req, groupInvitationPayload(inv))
}
@@ -5665,7 +5713,8 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic
writeIMRelationError(wrt, req, joinErr)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.member.joined",
map[string]any{"user_id": principal.Uid.UserId(), "joined": joined, "source": "open_group"})
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "status": "joined", "joined": joined, "source": "open_group"})
return
}
@@ -5694,7 +5743,8 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic
writeIMRelationError(wrt, req, err)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.join_request.created",
map[string]any{"request_id": item.RequestID, "user_id": principal.Uid.UserId()})
writeIMSuccess(wrt, req, groupJoinRequestPayload(item))
return
}
@@ -5723,6 +5773,7 @@ func serveIMGroupJoinRequests(wrt http.ResponseWriter, req *http.Request, topic
}
items = nil
}
items = dedupePendingGroupJoinRequests(items, status)
payloads, err := groupJoinRequestPayloadsWithUsers(principal.TenantID, items)
if err != nil {
writeIMRelationError(wrt, req, err)
@@ -5771,7 +5822,8 @@ func serveIMGroupJoinRequestAction(wrt http.ResponseWriter, req *http.Request, t
[]imSystemMessageAction{{Type: "group_profile", Label: "查看群聊", TargetID: topic}},
map[string]any{"request_id": updated.RequestID, "source": "approval"})
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.join_request."+updated.Status,
map[string]any{"request_id": updated.RequestID, "user_id": updated.ApplicantUserID.UserId(), "status": updated.Status})
writeIMSuccess(wrt, req, groupJoinRequestPayload(updated))
}
@@ -5843,7 +5895,8 @@ func serveIMGroupAnnouncements(wrt http.ResponseWriter, req *http.Request, topic
writeIMRelationError(wrt, req, err)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.announcement.updated",
map[string]any{"announcement_id": announcement.AnnouncementID, "topic": topic})
writeIMSuccess(wrt, req, groupAnnouncementPayloadWithPermissions(topic, announcement, selfSub))
case strings.HasPrefix(tail, "announcements/") && strings.HasSuffix(tail, "/read") && req.Method == http.MethodPost:
announcementID := strings.TrimSuffix(strings.TrimPrefix(tail, "announcements/"), "/read")
@@ -6001,7 +6054,8 @@ func serveIMGroupGovernance(wrt http.ResponseWriter, req *http.Request, topic st
nil,
map[string]any{"mute_all": *body.MuteAll, "version": settings.Version})
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.settings.updated",
map[string]any{"topic": topic, "version": settings.Version})
writeIMSuccess(wrt, req, groupGovernancePayloadFromSettings(settings, selfSub))
default:
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
@@ -6082,7 +6136,8 @@ func serveIMGroupAdmins(wrt http.ResponseWriter, req *http.Request, topic, tail
writeIMRelationError(wrt, req, err)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.role.updated",
map[string]any{"topic": topic, "results": results})
writeIMSuccess(wrt, req, map[string]any{"succeeded": results, "admins": groupAdminItems(members), "version": body.Version + 1, "updated_at": time.Now().UTC().Format(time.RFC3339Nano)})
case strings.HasPrefix(tail, "admins/") && req.Method == http.MethodDelete:
userID := strings.TrimPrefix(tail, "admins/")
@@ -6096,7 +6151,8 @@ func serveIMGroupAdmins(wrt http.ResponseWriter, req *http.Request, topic, tail
writeIMRelationError(wrt, req, err)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.role.updated",
map[string]any{"user_id": uid.UserId(), "role": "member"})
writeIMSuccess(wrt, req, map[string]any{"user_id": uid.UserId(), "role": "member", "updated_at": time.Now().UTC().Format(time.RFC3339Nano)})
default:
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
@@ -6120,6 +6176,9 @@ func serveIMGroupOwnerTransfer(wrt http.ResponseWriter, req *http.Request, topic
if !decodeIMJSONBody(wrt, req, &body) {
return
}
if !consumeIMActionTokenHeader(wrt, req, principal, "group_owner_transfer") {
return
}
target := types.ParseUserId(strings.TrimSpace(body.TargetUserID))
if target.IsZero() || target == principal.Uid {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "target_user_id"})
@@ -6135,7 +6194,8 @@ func serveIMGroupOwnerTransfer(wrt http.ResponseWriter, req *http.Request, topic
writeIMRelationError(wrt, req, err)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.owner.transferred",
map[string]any{"owner": target.UserId(), "previous_owner": principal.Uid.UserId()})
writeIMSuccess(wrt, req, map[string]any{"owner": target.UserId(), "previous_owner": principal.Uid.UserId(), "version": body.Version + 1})
}
@@ -6162,8 +6222,16 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
return
}
if len(body.Members) == 0 || len(body.Members) > 100 {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "members"})
return
for _, userID := range cleanStringList(body.UserIDs, 100) {
body.Members = append(body.Members, struct {
UserID string `json:"user_id"`
ExpiresAt string `json:"expires_at,omitempty"`
}{UserID: userID})
}
if len(body.Members) == 0 || len(body.Members) > 100 {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "members"})
return
}
}
now := time.Now().UTC().Round(time.Millisecond)
mutes := make([]types.GroupMute, 0, len(body.Members))
@@ -6185,7 +6253,8 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
}
mutes = append(mutes, *mute)
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.mute.updated",
map[string]any{"topic": topic, "items": groupMutePayloads(mutes)})
writeIMSuccess(wrt, req, map[string]any{"topic": topic, "items": groupMutePayloads(mutes)})
case strings.HasPrefix(tail, "mutes/") && req.Method == http.MethodDelete:
uid := types.ParseUserId(strings.TrimSpace(strings.TrimPrefix(tail, "mutes/")))
@@ -6197,7 +6266,8 @@ func serveIMGroupMutes(wrt http.ResponseWriter, req *http.Request, topic, tail s
writeIMRelationError(wrt, req, err)
return
}
dispatchIMGroupEvents(principal.TenantID)
publishIMGroupRealtimeEvent(principal.TenantID, topic, principal.Uid, "group.mute.updated",
map[string]any{"user_id": uid.UserId(), "muted": false})
writeIMSuccess(wrt, req, map[string]any{"user_id": uid.UserId(), "muted": false})
default:
writeIMError(wrt, req, http.StatusMethodNotAllowed, 30002, imReasonMethodNotAllowed, "method not allowed", nil)
@@ -6639,7 +6709,6 @@ func serveIMFriendRequestAction(wrt http.ResponseWriter, req *http.Request) {
{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)
if imReliableHTTPEventsEnabled(req) {
eventType := "friend.request." + status
if err = publishIMRelationEvent(principal.TenantID, principal.Uid, updated.ApplicantID,
@@ -7163,11 +7232,30 @@ func defaultIMActionTarget(action, fallback string) string {
switch action {
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password":
return "youth_mode"
case "group_owner_transfer", "group_dissolve":
return action
default:
return fallback
}
}
func consumeIMActionTokenHeader(wrt http.ResponseWriter, req *http.Request, principal *imPrincipal, action string) bool {
actionToken := strings.TrimSpace(req.Header.Get("X-Action-Token"))
if actionToken == "" {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "X-Action-Token"})
return false
}
if store.Security == nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
return false
}
if _, err := store.Security.ConsumeActionToken(principal.TenantID, principal.Uid, actionToken, action, defaultIMActionTarget(action, action), time.Now().UTC().Round(time.Millisecond)); err != nil {
writeIMError(wrt, req, http.StatusForbidden, 30042, imReasonActionToken, "action token invalid", nil)
return false
}
return true
}
func hashIMActionToken(tenantID types.TenantID, uid types.Uid, action, target, tokenID, secret string) string {
h := sha256.New()
h.Write([]byte(fmt.Sprintf("%d", tenantID)))
@@ -7930,6 +8018,26 @@ func groupJoinRequestPayloads(items []types.GroupJoinRequest) []map[string]any {
return out
}
func dedupePendingGroupJoinRequests(items []types.GroupJoinRequest, status string) []types.GroupJoinRequest {
if len(items) < 2 || status != types.GroupJoinRequestStatusPending {
return items
}
seen := make(map[string]struct{}, len(items))
deduped := make([]types.GroupJoinRequest, 0, len(items))
for i := len(items) - 1; i >= 0; i-- {
key := items[i].Topic + ":" + items[i].ApplicantUserID.UserId()
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
deduped = append(deduped, items[i])
}
for left, right := 0, len(deduped)-1; left < right; left, right = left+1, right-1 {
deduped[left], deduped[right] = deduped[right], deduped[left]
}
return deduped
}
func groupJoinRequestPayloadsWithUsers(tenantID types.TenantID, items []types.GroupJoinRequest) ([]map[string]any, error) {
payloads := groupJoinRequestPayloads(items)
if len(items) == 0 || store.Users == nil {
@@ -9327,7 +9435,8 @@ func userProfilePayload10(profile *types.UserProfile) map[string]any {
func validIMSecurityChallengeScene(scene string) bool {
switch strings.TrimSpace(scene) {
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password":
case "set_independent_password", "enable_youth_mode", "close_youth_mode", "reset_independent_password",
"group_owner_transfer", "group_dissolve":
return true
default:
return false
@@ -9930,7 +10039,6 @@ func requireIMGroupMember(wrt http.ResponseWriter, req *http.Request, topic stri
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "topic"})
return nil, nil, false
}
dispatchIMGroupEvents(principal.TenantID)
sub, err := store.Subs.Get(principal.TenantID, topic, principal.Uid, false)
if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
@@ -11297,6 +11405,7 @@ func broadcastIMConversationSystemMessage(tenantID types.TenantID, msg *types.Me
logs.Warn.Printf("im system message: hub route queue full topic=%s seq=%d", msg.Topic, msg.SeqId)
}
deliverIMConversationSystemMessageToP2PSessions(tenantID, msg, out)
publishRealtimeTopicMessage(msg.Topic, out)
broadcastIMConversationSystemPresence(tenantID, msg, timestamp)
}