package conversation import ( "context" "fmt" "testing" "time" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/controller" "github.com/openimsdk/open-im-server/v3/pkg/common/storage/model" "github.com/openimsdk/open-im-server/v3/pkg/util/hashutil" pbconversation "github.com/openimsdk/protocol/conversation" pbmsg "github.com/openimsdk/protocol/msg" "github.com/openimsdk/protocol/sdkws" "github.com/openimsdk/tools/db/pagination" "github.com/openimsdk/tools/mcontext" "go.mongodb.org/mongo-driver/bson/primitive" "google.golang.org/grpc" "google.golang.org/protobuf/proto" ) func testCtx(userID string) context.Context { return mcontext.WithOpUserIDContext(context.Background(), userID) } func TestGetFullOwnerConversationIDsFreshDeviceWithoutPaginationKeepsLegacyFullIDs(t *testing.T) { withReadInactiveConversationFilterEnabled(t, false) const conversationCount = 50000 ids := newConversationIDs(conversationCount) srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, }) if err != nil { t.Fatal(err) } if resp.Equal { t.Fatal("fresh device idHash=0 should not be equal to the server-side conversation ID hash") } if got := len(resp.ConversationIDs); got != conversationCount { t.Fatalf("expected fresh device sync to return all %d conversation IDs, got %d", conversationCount, got) } if resp.Total != conversationCount { t.Fatalf("expected total %d, got %d", conversationCount, resp.Total) } } func TestGetFullOwnerConversationIDsFreshDeviceWithPaginationReturnsPage(t *testing.T) { withReadInactiveConversationFilterEnabled(t, false) const conversationCount = 50000 ids := newConversationIDs(conversationCount) srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, Pagination: &sdkws.RequestPagination{ PageNumber: 2, ShowNumber: 100, }, }) if err != nil { t.Fatal(err) } if resp.Equal { t.Fatal("fresh device idHash=0 should not be equal to the server-side conversation ID hash") } if got := len(resp.ConversationIDs); got != 100 { t.Fatalf("expected paged sync to return 100 conversation IDs, got %d", got) } if resp.Total != conversationCount { t.Fatalf("expected total %d, got %d", conversationCount, resp.Total) } if resp.ConversationIDs[0] != ids[100] { t.Fatalf("expected first ID on page 2 to be %q, got %q", ids[100], resp.ConversationIDs[0]) } } func TestGetFullOwnerConversationIDsInvalidPaginationKeepsLegacyFullIDs(t *testing.T) { withReadInactiveConversationFilterEnabled(t, false) const conversationCount = 50000 ids := newConversationIDs(conversationCount) srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, Pagination: &sdkws.RequestPagination{ PageNumber: 0, ShowNumber: 100, }, }) if err != nil { t.Fatal(err) } if got := len(resp.ConversationIDs); got != conversationCount { t.Fatalf("expected invalid pagination to keep legacy full IDs, got %d", got) } } func TestGetFullOwnerConversationIDsMatchingHashReturnsNoIDs(t *testing.T) { ids := []string{"si_user_1", "si_user_2"} srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: hashutil.IdHash(ids), }) if err != nil { t.Fatal(err) } if !resp.Equal { t.Fatal("matching idHash should be reported as equal") } if len(resp.ConversationIDs) != 0 { t.Fatalf("expected matching idHash to omit conversation IDs, got %d", len(resp.ConversationIDs)) } if resp.Total != int64(len(ids)) { t.Fatalf("expected total %d, got %d", len(ids), resp.Total) } } func TestGetFullOwnerConversationIDsMatchingHashWithPaginationReturnsNoIDs(t *testing.T) { ids := []string{"si_user_1", "si_user_2"} srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: hashutil.IdHash(ids), Pagination: &sdkws.RequestPagination{ PageNumber: 1, ShowNumber: 1, }, }) if err != nil { t.Fatal(err) } if !resp.Equal { t.Fatal("matching idHash should be reported as equal") } if len(resp.ConversationIDs) != 0 { t.Fatalf("expected matching idHash to omit conversation IDs, got %d", len(resp.ConversationIDs)) } if resp.Total != int64(len(ids)) { t.Fatalf("expected total %d, got %d", len(ids), resp.Total) } } func TestGetFullOwnerConversationIDsReadInactiveKeepsLegacyWhenFlagOff(t *testing.T) { withReadInactiveConversationFilterEnabled(t, false) ids := []string{"si_user_1", "si_user_2", "si_user_3"} srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, msgClient: fakeMessageClient{seqs: map[string]*pbmsg.FullSyncSeqs{ "si_user_2": readInactiveSeq(time.Now().Add(-2 * time.Hour)), }}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, }) if err != nil { t.Fatal(err) } if got := len(resp.ConversationIDs); got != len(ids) { t.Fatalf("expected legacy sync to return all %d conversation IDs, got %d", len(ids), got) } if resp.Total != int64(len(ids)) { t.Fatalf("expected total %d, got %d", len(ids), resp.Total) } } func TestGetFullOwnerConversationIDsReadInactiveFiltersReadInactiveConversations(t *testing.T) { withReadInactiveConversationCountThreshold(t, 0) withReadInactiveConversationDuration(t, int64(time.Hour/time.Millisecond)) ids := []string{"si_user_1", "si_user_2", "si_user_3", "si_user_4", "si_user_5"} filteredIDs := []string{"si_user_1", "si_user_3"} now := time.Now() srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, msgClient: fakeMessageClient{seqs: map[string]*pbmsg.FullSyncSeqs{ "si_user_1": unreadSeq(now.Add(-2 * time.Hour)), "si_user_2": readInactiveSeq(now.Add(-2 * time.Hour)), "si_user_3": readActiveSeq(now.Add(-30 * time.Minute)), "si_user_4": readInactiveSeq(now.Add(-3 * time.Hour)), "si_user_5": clearedSeq(now.Add(-2 * time.Hour)), }}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, }) if err != nil { t.Fatal(err) } if !sameStrings(resp.ConversationIDs, filteredIDs) { t.Fatalf("expected filtered conversation IDs %v, got %v", filteredIDs, resp.ConversationIDs) } if resp.Total != int64(len(filteredIDs)) { t.Fatalf("expected filtered total %d, got %d", len(filteredIDs), resp.Total) } } func TestGetFullOwnerConversationIDsReadInactiveFiltersEmptyConversations(t *testing.T) { withReadInactiveConversationCountThreshold(t, 0) withReadInactiveConversationDuration(t, int64(time.Hour/time.Millisecond)) ids := []string{"si_user_1", "si_user_2", "si_user_3"} now := time.Now() srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, msgClient: fakeMessageClient{seqs: map[string]*pbmsg.FullSyncSeqs{ "si_user_1": {HasReadSeq: 0, MaxSeq: 0, MaxSeqTime: 0}, "si_user_2": readInactiveSeq(now.Add(-2 * time.Hour)), "si_user_3": nil, }}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, }) if err != nil { t.Fatal(err) } if !sameStrings(resp.ConversationIDs, []string{"si_user_3"}) { t.Fatalf("expected empty conversations to be filtered and missing seqs to be kept, got %v", resp.ConversationIDs) } if resp.Total != 1 { t.Fatalf("expected filtered total 1, got %d", resp.Total) } } func TestGetFullOwnerConversationIDsReadInactiveMatchingFilteredHashReturnsNoIDs(t *testing.T) { withReadInactiveConversationCountThreshold(t, 0) withReadInactiveConversationDuration(t, int64(time.Hour/time.Millisecond)) ids := []string{"si_user_1", "si_user_2", "si_user_3"} filteredIDs := []string{"si_user_1", "si_user_3"} now := time.Now() srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, msgClient: fakeMessageClient{seqs: map[string]*pbmsg.FullSyncSeqs{ "si_user_1": unreadSeq(now.Add(-2 * time.Hour)), "si_user_2": readInactiveSeq(now.Add(-2 * time.Hour)), "si_user_3": readActiveSeq(now.Add(-30 * time.Minute)), }}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: hashutil.IdHash(filteredIDs), }) if err != nil { t.Fatal(err) } if !resp.Equal { t.Fatal("matching filtered idHash should be reported as equal") } if len(resp.ConversationIDs) != 0 { t.Fatalf("expected matching filtered idHash to omit conversation IDs, got %d", len(resp.ConversationIDs)) } if resp.Total != int64(len(filteredIDs)) { t.Fatalf("expected filtered total %d, got %d", len(filteredIDs), resp.Total) } } func TestGetFullOwnerConversationIDsReadInactiveKeepsConversationsWithUnknownMaxSeqTime(t *testing.T) { withReadInactiveConversationCountThreshold(t, 0) withReadInactiveConversationDuration(t, int64(time.Hour/time.Millisecond)) ids := []string{"si_user_1", "si_user_2"} srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, msgClient: fakeMessageClient{seqs: map[string]*pbmsg.FullSyncSeqs{ "si_user_1": {HasReadSeq: 10, MaxSeq: 10, MaxSeqTime: 0}, "si_user_2": readInactiveSeq(time.Now().Add(-2 * time.Hour)), }}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, }) if err != nil { t.Fatal(err) } if !sameStrings(resp.ConversationIDs, []string{"si_user_1"}) { t.Fatalf("expected unknown maxSeqTime conversation to be kept, got %v", resp.ConversationIDs) } } func TestGetFullOwnerConversationIDsReadInactiveKeepsPinnedConversations(t *testing.T) { withReadInactiveConversationCountThreshold(t, 0) withReadInactiveConversationDuration(t, int64(time.Hour/time.Millisecond)) ids := []string{"si_user_1", "si_user_2", "si_user_3"} srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{ conversationIDs: ids, pinnedConversationIDs: []string{"si_user_1", "si_user_2"}, }, msgClient: fakeMessageClient{seqs: map[string]*pbmsg.FullSyncSeqs{ "si_user_1": {HasReadSeq: 0, MaxSeq: 0, MaxSeqTime: 0}, "si_user_2": readInactiveSeq(time.Now().Add(-2 * time.Hour)), "si_user_3": readInactiveSeq(time.Now().Add(-2 * time.Hour)), }}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, }) if err != nil { t.Fatal(err) } if !sameStrings(resp.ConversationIDs, []string{"si_user_1", "si_user_2"}) { t.Fatalf("expected pinned conversations to be kept, got %v", resp.ConversationIDs) } } func TestGetFullOwnerConversationIDsReadInactivePaginatesFilteredIDs(t *testing.T) { withReadInactiveConversationCountThreshold(t, 0) withReadInactiveConversationDuration(t, int64(time.Hour/time.Millisecond)) ids := []string{"si_user_1", "si_user_2", "si_user_3", "si_user_4", "si_user_5"} filteredIDs := []string{"si_user_1", "si_user_3", "si_user_5"} now := time.Now() srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, msgClient: fakeMessageClient{seqs: map[string]*pbmsg.FullSyncSeqs{ "si_user_1": unreadSeq(now.Add(-2 * time.Hour)), "si_user_2": readInactiveSeq(now.Add(-2 * time.Hour)), "si_user_3": readActiveSeq(now.Add(-30 * time.Minute)), "si_user_4": readInactiveSeq(now.Add(-3 * time.Hour)), "si_user_5": unreadSeq(now.Add(-3 * time.Hour)), }}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, Pagination: &sdkws.RequestPagination{ PageNumber: 2, ShowNumber: 1, }, }) if err != nil { t.Fatal(err) } if !sameStrings(resp.ConversationIDs, []string{filteredIDs[1]}) { t.Fatalf("expected second filtered page %v, got %v", []string{filteredIDs[1]}, resp.ConversationIDs) } if resp.Total != int64(len(filteredIDs)) { t.Fatalf("expected filtered total %d, got %d", len(filteredIDs), resp.Total) } } func BenchmarkGetFullOwnerConversationIDsLegacyFullIDs(b *testing.B) { readInactiveConversationFilterEnabled = false b.Cleanup(func() { readInactiveConversationFilterEnabled = true }) ids := newConversationIDs(50000) srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, } req := &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, } b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), req) if err != nil { b.Fatal(err) } b.ReportMetric(float64(len(resp.ConversationIDs)), "ids/op") b.ReportMetric(float64(proto.Size(resp)), "proto_bytes/op") } } func BenchmarkGetFullOwnerConversationIDsReadInactiveFilteredIDs(b *testing.B) { readInactiveConversationDuration = int64(time.Hour / time.Millisecond) b.Cleanup(func() { readInactiveConversationDuration = int64((30 * 24 * time.Hour) / time.Millisecond) }) ids := newConversationIDs(50000) seqs := make(map[string]*pbmsg.FullSyncSeqs, len(ids)) now := time.Now() for i, conversationID := range ids { if i%10 == 0 { seqs[conversationID] = unreadSeq(now.Add(-2 * time.Hour)) } else { seqs[conversationID] = readInactiveSeq(now.Add(-2 * time.Hour)) } } srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, msgClient: fakeMessageClient{seqs: seqs}, } req := &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, } b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), req) if err != nil { b.Fatal(err) } b.ReportMetric(float64(len(resp.ConversationIDs)), "ids/op") b.ReportMetric(float64(proto.Size(resp)), "proto_bytes/op") } } func TestGetFullOwnerConversationIDsReadInactiveSkipsSmallConversationSetsByDefault(t *testing.T) { ids := []string{"si_user_1", "si_user_2", "si_user_3"} srv := &conversationServer{ conversationDatabase: &fakeConversationDatabase{conversationIDs: ids}, msgClient: fakeMessageClient{seqs: map[string]*pbmsg.FullSyncSeqs{ "si_user_1": readInactiveSeq(time.Now().Add(-2 * time.Hour)), "si_user_2": readInactiveSeq(time.Now().Add(-2 * time.Hour)), "si_user_3": readInactiveSeq(time.Now().Add(-2 * time.Hour)), }}, } resp, err := srv.GetFullOwnerConversationIDs(testCtx("customer-service"), &pbconversation.GetFullOwnerConversationIDsReq{ UserID: "customer-service", IdHash: 0, }) if err != nil { t.Fatal(err) } if !sameStrings(resp.ConversationIDs, ids) { t.Fatalf("expected small conversation sets to keep legacy IDs, got %v", resp.ConversationIDs) } } func readInactiveSeq(maxSeqTime time.Time) *pbmsg.FullSyncSeqs { return &pbmsg.FullSyncSeqs{ HasReadSeq: 10, MaxSeq: 10, MaxSeqTime: maxSeqTime.UnixMilli(), } } func readActiveSeq(maxSeqTime time.Time) *pbmsg.FullSyncSeqs { return &pbmsg.FullSyncSeqs{ HasReadSeq: 10, MaxSeq: 10, MaxSeqTime: maxSeqTime.UnixMilli(), } } func clearedSeq(maxSeqTime time.Time) *pbmsg.FullSyncSeqs { return &pbmsg.FullSyncSeqs{ HasReadSeq: 10, MaxSeq: 10, MaxSeqTime: maxSeqTime.UnixMilli(), UserMinSeq: 11, } } func unreadSeq(maxSeqTime time.Time) *pbmsg.FullSyncSeqs { return &pbmsg.FullSyncSeqs{ HasReadSeq: 9, MaxSeq: 10, MaxSeqTime: maxSeqTime.UnixMilli(), } } func newConversationIDs(count int) []string { ids := make([]string, 0, count) for i := 0; i < count; i++ { ids = append(ids, fmt.Sprintf("si_user_%05d", i)) } return ids } func sameStrings(a, b []string) bool { if len(a) != len(b) { return false } for i := range a { if a[i] != b[i] { return false } } return true } func withReadInactiveConversationCountThreshold(t *testing.T, threshold int) { t.Helper() old := readInactiveConversationCountThreshold readInactiveConversationCountThreshold = threshold t.Cleanup(func() { readInactiveConversationCountThreshold = old }) } func withReadInactiveConversationFilterEnabled(t *testing.T, enabled bool) { t.Helper() old := readInactiveConversationFilterEnabled readInactiveConversationFilterEnabled = enabled t.Cleanup(func() { readInactiveConversationFilterEnabled = old }) } func withReadInactiveConversationDuration(t *testing.T, duration int64) { t.Helper() old := readInactiveConversationDuration readInactiveConversationDuration = duration t.Cleanup(func() { readInactiveConversationDuration = old }) } type fakeConversationDatabase struct { conversationIDs []string pinnedConversationIDs []string } func (f *fakeConversationDatabase) UpdateUsersConversationField(context.Context, []string, string, map[string]any) error { return nil } func (f *fakeConversationDatabase) CreateConversation(context.Context, []*model.Conversation) error { return nil } func (f *fakeConversationDatabase) SyncPeerUserPrivateConversationTx(context.Context, []*model.Conversation) error { return nil } func (f *fakeConversationDatabase) FindConversations(_ context.Context, _ string, conversationIDs []string) ([]*model.Conversation, error) { conversations := make([]*model.Conversation, 0, len(conversationIDs)) for _, conversationID := range conversationIDs { conversations = append(conversations, &model.Conversation{ConversationID: conversationID}) } return conversations, nil } func (f *fakeConversationDatabase) GetUserAllConversation(context.Context, string) ([]*model.Conversation, error) { return nil, nil } func (f *fakeConversationDatabase) SetUserConversations(context.Context, string, []*model.Conversation) error { return nil } func (f *fakeConversationDatabase) SetUsersConversationFieldTx(context.Context, []string, *model.Conversation, map[string]any) error { return nil } func (f *fakeConversationDatabase) UpdateUserConversations(context.Context, string, map[string]any) error { return nil } func (f *fakeConversationDatabase) CreateGroupChatConversation(context.Context, string, []string, *model.Conversation) error { return nil } func (f *fakeConversationDatabase) GetConversationIDs(context.Context, string) ([]string, error) { return f.conversationIDs, nil } func (f *fakeConversationDatabase) GetUserConversationIDsHash(context.Context, string) (uint64, error) { return hashutil.IdHash(f.conversationIDs), nil } func (f *fakeConversationDatabase) GetAllConversationIDs(context.Context) ([]string, error) { return nil, nil } func (f *fakeConversationDatabase) GetAllConversationIDsNumber(context.Context) (int64, error) { return 0, nil } func (f *fakeConversationDatabase) PageConversationIDs(context.Context, pagination.Pagination) ([]string, error) { return nil, nil } func (f *fakeConversationDatabase) GetConversationsByConversationID(context.Context, []string) ([]*model.Conversation, error) { return nil, nil } func (f *fakeConversationDatabase) GetConversationIDsNeedDestruct(context.Context) ([]*model.Conversation, error) { return nil, nil } func (f *fakeConversationDatabase) GetConversationNotReceiveMessageUserIDs(context.Context, string) ([]string, error) { return nil, nil } func (f *fakeConversationDatabase) FindConversationUserVersion(context.Context, string, uint, int) (*model.VersionLog, error) { return nil, nil } func (f *fakeConversationDatabase) FindMaxConversationUserVersionCache(context.Context, string) (*model.VersionLog, error) { return &model.VersionLog{ID: primitive.NewObjectID(), LastUpdate: time.Now()}, nil } func (f *fakeConversationDatabase) GetOwnerConversation(context.Context, string, pagination.Pagination) (int64, []*model.Conversation, error) { return int64(len(f.conversationIDs)), nil, nil } func (f *fakeConversationDatabase) GetNotNotifyConversationIDs(context.Context, string) ([]string, error) { return nil, nil } func (f *fakeConversationDatabase) GetPinnedConversationIDs(context.Context, string) ([]string, error) { return f.pinnedConversationIDs, nil } func (f *fakeConversationDatabase) FindRandConversation(context.Context, int64, int) ([]*model.Conversation, error) { return nil, nil } func (f *fakeConversationDatabase) DeleteUsersConversations(context.Context, string, []string) error { return nil } var _ controller.ConversationDatabase = (*fakeConversationDatabase)(nil) type fakeMessageClient struct { seqs map[string]*pbmsg.FullSyncSeqs } func (f fakeMessageClient) GetMaxSeqs(context.Context, []string) (map[string]int64, error) { return nil, nil } func (f fakeMessageClient) GetMsgByConversationIDs(context.Context, []string, map[string]int64) (map[string]*sdkws.MsgData, error) { return nil, nil } func (f fakeMessageClient) GetHasReadSeqs(context.Context, []string, string) (map[string]int64, error) { return nil, nil } func (f fakeMessageClient) GetConversationsFullSyncSeqs(_ context.Context, req *pbmsg.GetConversationsFullSyncSeqsReq) (*pbmsg.GetConversationsFullSyncSeqsResp, error) { seqs := make(map[string]*pbmsg.FullSyncSeqs, len(req.ConversationIDs)) for _, conversationID := range req.ConversationIDs { if seq, ok := f.seqs[conversationID]; ok { seqs[conversationID] = seq } } return &pbmsg.GetConversationsFullSyncSeqsResp{Seqs: seqs}, nil } func (f fakeMessageClient) SetUserConversationMaxSeq(context.Context, string, []string, int64) error { return nil } func (f fakeMessageClient) SetUserConversationMin(context.Context, string, []string, int64) error { return nil } func (f fakeMessageClient) GetLastMessageSeqByTime(context.Context, string, int64) (int64, error) { return 0, nil } func (f fakeMessageClient) GetLastMessage(context.Context, *pbmsg.GetLastMessageReq, ...grpc.CallOption) (*pbmsg.GetLastMessageResp, error) { return &pbmsg.GetLastMessageResp{}, nil }