diff --git a/server/im_http_test.go b/server/im_http_test.go index 5cf4aad..dd44ec9 100644 --- a/server/im_http_test.go +++ b/server/im_http_test.go @@ -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 } diff --git a/server/init_topic.go b/server/init_topic.go index f73db82..90c4528 100644 --- a/server/init_topic.go +++ b/server/init_topic.go @@ -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 diff --git a/server/init_topic_group_business_test.go b/server/init_topic_group_business_test.go new file mode 100644 index 0000000..9085ba8 --- /dev/null +++ b/server/init_topic_group_business_test.go @@ -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) + } +}