fix: persist settings for websocket groups

This commit is contained in:
richjohnrambo
2026-09-21 11:58:36 +08:00
parent c891afba73
commit 05ad37ba80
3 changed files with 168 additions and 2 deletions
+4
View File
@@ -304,6 +304,7 @@ type testGroupBusinessStore struct {
leftTopic string
leftUID types.Uid
updateSettingsErr error
getSettingsErr error
updateMemberErr error
}
@@ -319,6 +320,9 @@ func (s *testGroupBusinessStore) CreateSettings(_ types.TenantID, settings *type
}
func (s *testGroupBusinessStore) GetSettings(_ types.TenantID, topic string) (*types.GroupSettings, error) {
if s.getSettingsErr != nil {
return nil, s.getSettingsErr
}
if s.settings != nil {
return s.settings, nil
}
+57 -2
View File
@@ -9,7 +9,9 @@
package main
import (
"errors"
"strings"
"time"
"github.com/tinode/chat/server/auth"
"github.com/tinode/chat/server/logs"
@@ -602,9 +604,15 @@ func initTopicNewGrp(t *Topic, sreg *ClientComMessage, isChan bool) error {
Trusted: t.trusted,
}
// store.Topics.Create will add a subscription record for the topic creator
// Persist the topic and its owner subscription together with product group metadata.
stopic.GiveAccess(t.owner, userData.modeWant, userData.modeGiven)
err := store.Topics.Create(t.tenantID, stopic, t.owner, t.perUser[t.owner].private)
var err error
if !isChan && store.GroupBusiness != nil {
err = store.GroupBusiness.CreateGroup(t.tenantID, stopic, t.owner, t.perUser[t.owner].private,
newWebsocketGroupSettings(stopic, t.owner, timestamp), nil, nil, timestamp)
} else {
err = store.Topics.Create(t.tenantID, stopic, t.owner, t.perUser[t.owner].private)
}
if err != nil {
return err
}
@@ -626,6 +634,28 @@ func initTopicNewGrp(t *Topic, sreg *ClientComMessage, isChan bool) error {
return nil
}
func newWebsocketGroupSettings(topic *types.Topic, owner types.Uid, now time.Time) *types.GroupSettings {
public := groupPublicMap(topic.Public)
description, _ := imPublicString(public, "description")
return &types.GroupSettings{
Topic: topic.Id,
OwnerUserID: owner,
Name: groupName(public, topic.Id),
AvatarFileID: imAvatarFileIDFromPublic(public),
Description: description,
GroupStatus: types.GroupStatusActive,
MemberUploadFiles: true,
MemberPrivateChat: true,
MemberInvite: true,
MaxAdminCount: groupMaxAdminCount,
MemberCount: 1,
Version: 1,
CreatedBy: owner,
CreatedAt: now,
UpdatedAt: now,
}
}
// Initialize existing group topic. There is a race condition when two users attempt to load
// the same topic at the same time. It's prevented at hub level.
func initTopicGrp(t *Topic) error {
@@ -671,6 +701,11 @@ func initTopicGrp(t *Topic) error {
t.lastID = stopic.SeqId
t.delID = stopic.DelId
t.subCnt = stopic.SubCnt
if !t.isChan {
if err = ensureLegacyWebsocketGroupSettings(t.tenantID, stopic, t.owner, len(t.perUser)); err != nil {
return err
}
}
// Initialize channel for receiving session online updates.
t.supd = make(chan *sessionUpdate, 32)
@@ -680,6 +715,26 @@ func initTopicGrp(t *Topic) error {
return nil
}
func ensureLegacyWebsocketGroupSettings(tenantID types.TenantID, topic *types.Topic, owner types.Uid, memberCount int) error {
if store.GroupBusiness == nil || topic == nil || owner.IsZero() {
return nil
}
if _, err := store.GroupBusiness.GetSettings(tenantID, topic.Id); err == nil {
return nil
} else if errors.Is(err, types.ErrUnsupported) {
return nil
} else if !errors.Is(err, types.ErrNotFound) {
return err
}
settings := newWebsocketGroupSettings(topic, owner, topic.CreatedAt)
settings.MemberCount = memberCount
if !topic.UpdatedAt.IsZero() {
settings.UpdatedAt = topic.UpdatedAt
}
_, err := store.GroupBusiness.CreateSettings(tenantID, settings)
return err
}
// Initialize system topic. System topic is a singleton, always in memory.
func initTopicSys(t *Topic) error {
t.cat = types.TopicCatSys
+107
View File
@@ -0,0 +1,107 @@
package main
import (
"testing"
"time"
"github.com/tinode/chat/server/auth"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
func TestInitTopicNewGrpCreatesBusinessSettings(t *testing.T) {
previous := store.GroupBusiness
groupStore := &testGroupBusinessStore{}
store.GroupBusiness = groupStore
defer func() { store.GroupBusiness = previous }()
owner := types.Uid(42)
topic := &Topic{
tenantID: 7,
name: "grpWebsocket",
xoriginal: "new",
perUser: make(map[types.Uid]perUserData),
}
request := &ClientComMessage{
RcptTo: topic.name,
AsUser: owner.UserId(),
AuthLvl: int(auth.LevelAuth),
Sub: &MsgClientSub{Topic: "new", Set: &MsgSetQuery{Desc: &MsgSetDesc{Public: map[string]any{
"fn": "WebSocket 测试群",
"description": "业务资料需要原子创建",
}}}},
}
if err := initTopicNewGrp(topic, request, false); err != nil {
t.Fatal(err)
}
if groupStore.settings == nil || groupStore.settings.Topic != topic.name || groupStore.settings.OwnerUserID != owner {
t.Fatalf("business settings were not created: %#v", groupStore.settings)
}
if !request.Sub.Created || !request.Sub.Newsub || topic.subCnt != 1 || topic.xoriginal != topic.name {
t.Fatalf("unexpected initialized topic: created=%v newsub=%v subcnt=%d original=%q",
request.Sub.Created, request.Sub.Newsub, topic.subCnt, topic.xoriginal)
}
}
func TestNewWebsocketGroupSettings(t *testing.T) {
now := types.TimeNow()
owner := types.Uid(42)
topic := &types.Topic{
ObjHeader: types.ObjHeader{Id: "grpWebsocket"},
Public: map[string]any{
"fn": "WebSocket 测试群",
"avatar_file_id": "avatar.png",
"description": "通过 sub topic:new 创建",
},
}
settings := newWebsocketGroupSettings(topic, owner, now)
if settings.Topic != topic.Id || settings.OwnerUserID != owner || settings.CreatedBy != owner {
t.Fatalf("unexpected group identity: %#v", settings)
}
if settings.Name != "WebSocket 测试群" || settings.AvatarFileID != "avatar.png" ||
settings.Description != "通过 sub topic:new 创建" {
t.Fatalf("unexpected group profile: %#v", settings)
}
if settings.GroupStatus != types.GroupStatusActive || !settings.MemberUploadFiles ||
!settings.MemberPrivateChat || !settings.MemberInvite || settings.MemberCount != 1 ||
settings.MaxAdminCount != groupMaxAdminCount || settings.Version != 1 {
t.Fatalf("unexpected group defaults: %#v", settings)
}
if !settings.CreatedAt.Equal(now) || !settings.UpdatedAt.Equal(now) {
t.Fatalf("unexpected group timestamps: %#v", settings)
}
}
func TestNewWebsocketGroupSettingsFallsBackToTopicName(t *testing.T) {
topic := &types.Topic{ObjHeader: types.ObjHeader{Id: "grpFallback"}}
settings := newWebsocketGroupSettings(topic, types.Uid(7), types.TimeNow())
if settings.Name != topic.Id {
t.Fatalf("name: got %q, want %q", settings.Name, topic.Id)
}
}
func TestEnsureLegacyWebsocketGroupSettingsRepairsMissingRecord(t *testing.T) {
previous := store.GroupBusiness
groupStore := &testGroupBusinessStore{getSettingsErr: types.ErrNotFound}
store.GroupBusiness = groupStore
defer func() { store.GroupBusiness = previous }()
created := types.TimeNow().Add(-time.Hour)
updated := types.TimeNow()
topic := &types.Topic{
ObjHeader: types.ObjHeader{Id: "grpLegacy", CreatedAt: created, UpdatedAt: updated},
Public: map[string]any{"fn": "历史 WebSocket 群"},
}
owner := types.Uid(42)
if err := ensureLegacyWebsocketGroupSettings(7, topic, owner, 3); err != nil {
t.Fatal(err)
}
if groupStore.settings == nil || groupStore.settings.Topic != topic.Id || groupStore.settings.MemberCount != 3 {
t.Fatalf("legacy settings were not repaired: %#v", groupStore.settings)
}
if !groupStore.settings.CreatedAt.Equal(created) || !groupStore.settings.UpdatedAt.Equal(updated) {
t.Fatalf("legacy timestamps were not preserved: %#v", groupStore.settings)
}
}