From 78dc739d393ece581ecf0c876d50861c5f1c7186 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 15 May 2024 16:09:26 -0400 Subject: [PATCH 01/40] PoC in memory two way messaging --- go.mod | 2 +- go.sum | 4 +- pkg/code/chat/message_code_team.go | 4 +- pkg/code/chat/message_kin_purchases.go | 12 +- pkg/code/chat/sender_test.go | 4 +- pkg/code/push/notifications.go | 12 +- pkg/code/server/grpc/chat/server.go | 261 ++++++++++++++++++-- pkg/code/server/grpc/chat/server_test.go | 22 +- pkg/code/server/grpc/chat/stream.go | 121 +++++++++ pkg/code/server/grpc/messaging/server.go | 10 +- pkg/code/server/grpc/messaging/testutil.go | 4 +- pkg/code/server/grpc/transaction/v2/swap.go | 4 +- 12 files changed, 405 insertions(+), 55 deletions(-) create mode 100644 pkg/code/server/grpc/chat/stream.go diff --git a/go.mod b/go.mod index 7d06e3a5..ce7a4ec7 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.2 + github.com/code-payments/code-protobuf-api v1.16.3-0.20240515163011-81178a652d87 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index 4d97be47..8224ed64 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.2 h1:SH8I+JOYOnoARJ5h3jDX92vcoHeqm8UCQBlqP5516rI= -github.com/code-payments/code-protobuf-api v1.16.2/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.16.3-0.20240515163011-81178a652d87 h1:uKU2M+0A0x72AzWjJvH9L5iZohDOnhVeDKpE0da+vKc= +github.com/code-payments/code-protobuf-api v1.16.3-0.20240515163011-81178a652d87/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/pkg/code/chat/message_code_team.go b/pkg/code/chat/message_code_team.go index fe8a7049..536370a3 100644 --- a/pkg/code/chat/message_code_team.go +++ b/pkg/code/chat/message_code_team.go @@ -48,8 +48,8 @@ func newIncentiveMessage(localizedTextKey string, intentRecord *intent.Record) ( content := []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localizedTextKey, }, }, diff --git a/pkg/code/chat/message_kin_purchases.go b/pkg/code/chat/message_kin_purchases.go index 1ec10b68..f9a2c6fd 100644 --- a/pkg/code/chat/message_kin_purchases.go +++ b/pkg/code/chat/message_kin_purchases.go @@ -40,8 +40,8 @@ func SendKinPurchasesMessage(ctx context.Context, data code_data.Provider, recei func ToUsdcDepositedMessage(signature string, ts time.Time) (*chatpb.ChatMessage, error) { content := []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localization.ChatMessageUsdcDeposited, }, }, @@ -60,8 +60,8 @@ func NewUsdcBeingConvertedMessage(ts time.Time) (*chatpb.ChatMessage, error) { content := []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localization.ChatMessageUsdcBeingConverted, }, }, @@ -79,8 +79,8 @@ func ToKinAvailableForUseMessage(signature string, ts time.Time, purchases ...*t content := []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localization.ChatMessageKinAvailableForUse, }, }, diff --git a/pkg/code/chat/sender_test.go b/pkg/code/chat/sender_test.go index 7625a1f3..3438b3c8 100644 --- a/pkg/code/chat/sender_test.go +++ b/pkg/code/chat/sender_test.go @@ -159,8 +159,8 @@ func newRandomChatMessage(t *testing.T, contentLength int) *chatpb.ChatMessage { var content []*chatpb.Content for i := 0; i < contentLength; i++ { content = append(content, &chatpb.Content{ - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: fmt.Sprintf("key%d", rand.Uint32()), }, }, diff --git a/pkg/code/push/notifications.go b/pkg/code/push/notifications.go index ccc361fb..42aff9be 100644 --- a/pkg/code/push/notifications.go +++ b/pkg/code/push/notifications.go @@ -320,15 +320,15 @@ func SendChatMessagePushNotification( for _, content := range chatMessage.Content { var contentToPush *chatpb.Content switch typedContent := content.Type.(type) { - case *chatpb.Content_Localized: - localizedPushBody, err := localization.Localize(locale, typedContent.Localized.KeyOrText) + case *chatpb.Content_ServerLocalized: + localizedPushBody, err := localization.Localize(locale, typedContent.ServerLocalized.KeyOrText) if err != nil { continue } contentToPush = &chatpb.Content{ - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localizedPushBody, }, }, @@ -358,8 +358,8 @@ func SendChatMessagePushNotification( } contentToPush = &chatpb.Content{ - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: localizedPushBody, }, }, diff --git a/pkg/code/server/grpc/chat/server.go b/pkg/code/server/grpc/chat/server.go index 362c0fd7..b66ac9fe 100644 --- a/pkg/code/server/grpc/chat/server.go +++ b/pkg/code/server/grpc/chat/server.go @@ -1,14 +1,20 @@ package chat import ( + "bytes" "context" + "fmt" "math" + "strings" + "sync" + "time" "github.com/mr-tron/base58" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" @@ -28,19 +34,28 @@ const ( maxPageSize = 100 ) +var ( + mockTwoWayChat = chat.GetChatId("user1", "user2", true).ToProto() +) + +// todo: Resolve duplication of streaming logic with messaging service. The latest and greatest will live here. type server struct { log *logrus.Entry data code_data.Provider auth *auth_util.RPCSignatureVerifier + streamsMu sync.RWMutex + streams map[string]*chatEventStream + chatpb.UnimplementedChatServer } func NewChatServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier) chatpb.ChatServer { return &server{ - log: logrus.StandardLogger().WithField("type", "chat/server"), - data: data, - auth: auth, + log: logrus.StandardLogger().WithField("type", "chat/server"), + data: data, + auth: auth, + streams: make(map[string]*chatEventStream), } } @@ -147,7 +162,7 @@ func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*ch } protoMetadata.Title = &chatpb.ChatMetadata_Localized{ - Localized: &chatpb.LocalizedContent{ + Localized: &chatpb.ServerLocalizedContent{ KeyOrText: localization.LocalizeWithFallback( locale, localization.GetLocalizationKeyForUserAgent(ctx, chatProperties.TitleLocalizationKey), @@ -298,11 +313,11 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest for _, content := range protoChatMessage.Content { switch typed := content.Type.(type) { - case *chatpb.Content_Localized: - typed.Localized.KeyOrText = localization.LocalizeWithFallback( + case *chatpb.Content_ServerLocalized: + typed.ServerLocalized.KeyOrText = localization.LocalizeWithFallback( locale, - localization.GetLocalizationKeyForUserAgent(ctx, typed.Localized.KeyOrText), - typed.Localized.KeyOrText, + localization.GetLocalizationKeyForUserAgent(ctx, typed.ServerLocalized.KeyOrText), + typed.ServerLocalized.KeyOrText, ) } } @@ -347,23 +362,54 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR } log = log.WithField("owner_account", owner.PublicKey().ToBase58()) - chatId := chat.ChatIdFromProto(req.ChatId) - log = log.WithField("chat_id", chatId.String()) + signature := req.Signature + req.Signature = nil + if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { + return nil, err + } + chatId := chat.ChatIdFromProto(req.ChatId) messageId := base58.Encode(req.Pointer.Value.Value) log = log.WithFields(logrus.Fields{ + "chat_id": chatId.String(), "message_id": messageId, "pointer_type": req.Pointer.Kind, }) - if req.Pointer.Kind != chatpb.Pointer_READ { - return nil, status.Error(codes.InvalidArgument, "Pointer.Kind must be READ") + // todo: Temporary code to simluate real-time + if req.Pointer.User != nil { + return nil, status.Error(codes.InvalidArgument, "pointer.user cannot be set by clients") } + if bytes.Equal(mockTwoWayChat.Value, req.ChatId.Value) { + req.Pointer.User = &chatpb.ChatMemberId{Value: req.Owner.Value} - signature := req.Signature - req.Signature = nil - if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { - return nil, err + event := &chatpb.ChatStreamEvent{ + Pointers: []*chatpb.Pointer{req.Pointer}, + } + + s.streamsMu.RLock() + for key, stream := range s.streams { + if !strings.HasPrefix(key, chatId.String()) { + continue + } + + if strings.HasSuffix(key, owner.PublicKey().ToBase58()) { + continue + } + + if err := stream.notify(event, streamNotifyTimeout); err != nil { + log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) + } + } + s.streamsMu.RUnlock() + + return &chatpb.AdvancePointerResponse{ + Result: chatpb.AdvancePointerResponse_OK, + }, nil + } + + if req.Pointer.Kind != chatpb.Pointer_READ { + return nil, status.Error(codes.InvalidArgument, "Pointer.Kind must be READ") } chatRecord, err := s.data.GetChatById(ctx, chatId) @@ -529,3 +575,186 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr Result: chatpb.SetSubscriptionStateResponse_OK, }, nil } + +// +// Experimental PoC two-way chat APIs below +// + +func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) error { + ctx := streamer.Context() + + log := s.log.WithField("method", "StreamChatEvents") + log = client.InjectLoggingMetadata(ctx, log) + + req, err := boundedStreamChatEventsRecv(ctx, streamer, 250*time.Millisecond) + if err != nil { + return err + } + + if req.GetOpenStream() == nil { + return status.Error(codes.InvalidArgument, "open_stream is nil") + } + + if req.GetOpenStream().Signature == nil { + return status.Error(codes.InvalidArgument, "signature is nil") + } + + if !bytes.Equal(req.GetOpenStream().ChatId.Value, mockTwoWayChat.Value) { + return status.Error(codes.Unimplemented, "") + } + chatId := chat.ChatIdFromProto(req.GetOpenStream().ChatId) + log = log.WithField("chat_id", chatId.String()) + + owner, err := common.NewAccountFromProto(req.GetOpenStream().Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return status.Error(codes.Internal, "") + } + log = log.WithField("owner", owner.PublicKey().ToBase58()) + + signature := req.GetOpenStream().Signature + req.GetOpenStream().Signature = nil + if err = s.auth.Authenticate(streamer.Context(), owner, req.GetOpenStream(), signature); err != nil { + return err + } + + streamKey := fmt.Sprintf("%s:%s", chatId.String(), owner.PublicKey().ToBase58()) + + s.streamsMu.Lock() + + stream, exists := s.streams[streamKey] + if exists { + s.streamsMu.Unlock() + // There's an existing stream on this server that must be terminated first. + // Warn to see how often this happens in practice + log.Warnf("existing stream detected on this server (stream=%p) ; aborting", stream) + return status.Error(codes.Aborted, "stream already exists") + } + + stream = newChatEventStream(streamBufferSize) + + // The race detector complains when reading the stream pointer ref outside of the lock. + streamRef := fmt.Sprintf("%p", stream) + log.Tracef("setting up new stream (stream=%s)", streamRef) + s.streams[streamKey] = stream + + s.streamsMu.Unlock() + + sendPingCh := time.After(0) + streamHealthCh := monitorChatEventStreamHealth(ctx, log, streamRef, streamer) + + for { + select { + case event, ok := <-stream.streamCh: + if !ok { + log.Tracef("stream closed ; ending stream (stream=%s)", streamRef) + return status.Error(codes.Aborted, "stream closed") + } + + err := streamer.Send(&chatpb.StreamChatEventsResponse{ + Type: &chatpb.StreamChatEventsResponse_Events{ + Events: &chatpb.ChatStreamEventBatch{ + Events: []*chatpb.ChatStreamEvent{event}, + }, + }, + }) + if err != nil { + log.WithError(err).Info("failed to forward chat message") + return err + } + case <-sendPingCh: + log.Tracef("sending ping to client (stream=%s)", streamRef) + + sendPingCh = time.After(streamPingDelay) + + err := streamer.Send(&chatpb.StreamChatEventsResponse{ + Type: &chatpb.StreamChatEventsResponse_Ping{ + Ping: &commonpb.ServerPing{ + Timestamp: timestamppb.Now(), + PingDelay: durationpb.New(streamPingDelay), + }, + }, + }) + if err != nil { + log.Tracef("stream is unhealthy ; aborting (stream=%s)", streamRef) + return status.Error(codes.Aborted, "terminating unhealthy stream") + } + case <-streamHealthCh: + log.Tracef("stream is unhealthy ; aborting (stream=%s)", streamRef) + return status.Error(codes.Aborted, "terminating unhealthy stream") + case <-ctx.Done(): + log.Tracef("stream context cancelled ; ending stream (stream=%s)", streamRef) + return status.Error(codes.Canceled, "") + } + } +} + +func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest) (*chatpb.SendMessageResponse, error) { + log := s.log.WithField("method", "SendMessage") + log = client.InjectLoggingMetadata(ctx, log) + + if !bytes.Equal(req.ChatId.Value, mockTwoWayChat.Value) { + return nil, status.Error(codes.Unimplemented, "") + } + chatId := chat.ChatIdFromProto(req.ChatId) + log = log.WithField("chat_id", chatId.String()) + + owner, err := common.NewAccountFromProto(req.Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("owner", owner.PublicKey().ToBase58()) + + signature := req.Signature + req.Signature = nil + if err = s.auth.Authenticate(ctx, owner, req, signature); err != nil { + return nil, err + } + + switch req.Content[0].Type.(type) { + case *chatpb.Content_UserText: + default: + return nil, status.Error(codes.InvalidArgument, "content[0] must be UserText") + } + + // todo: Revisit message IDs + messageId, err := common.NewRandomAccount() + if err != nil { + log.WithError(err).Warn("failure generating random message id") + return nil, status.Error(codes.Internal, "") + } + + chatMessage := &chatpb.ChatMessage{ + MessageId: &chatpb.ChatMessageId{Value: messageId.ToProto().Value}, + Ts: timestamppb.Now(), + Content: req.Content, + Sender: &chatpb.ChatMemberId{Value: req.Owner.Value}, + Cursor: nil, // todo: Don't have cursor until we save it to the DB + } + + event := &chatpb.ChatStreamEvent{ + Messages: []*chatpb.ChatMessage{chatMessage}, + } + + s.streamsMu.RLock() + for key, stream := range s.streams { + if !strings.HasPrefix(key, chatId.String()) { + continue + } + + if strings.HasSuffix(key, owner.PublicKey().ToBase58()) { + continue + } + + if err := stream.notify(event, streamNotifyTimeout); err != nil { + log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) + } + } + s.streamsMu.RUnlock() + + return &chatpb.SendMessageResponse{ + Result: chatpb.SendMessageResponse_OK, + Message: chatMessage, + }, nil +} diff --git a/pkg/code/server/grpc/chat/server_test.go b/pkg/code/server/grpc/chat/server_test.go index f6dd6eff..627764b0 100644 --- a/pkg/code/server/grpc/chat/server_test.go +++ b/pkg/code/server/grpc/chat/server_test.go @@ -102,8 +102,8 @@ func TestGetChatsAndMessages_HappyPath(t *testing.T) { Ts: timestamppb.Now(), Content: []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: "msg.body.key", }, }, @@ -242,7 +242,7 @@ func TestGetChatsAndMessages_HappyPath(t *testing.T) { require.Len(t, getMessagesResp.Messages, 1) assert.Equal(t, expectedCodeTeamMessage.MessageId.Value, getMessagesResp.Messages[0].Cursor.Value) getMessagesResp.Messages[0].Cursor = nil - expectedCodeTeamMessage.Content[0].GetLocalized().KeyOrText = "localized message body content" + expectedCodeTeamMessage.Content[0].GetServerLocalized().KeyOrText = "localized message body content" assert.True(t, proto.Equal(expectedCodeTeamMessage, getMessagesResp.Messages[0])) getMessagesResp, err = env.client.GetMessages(env.ctx, getCashTransactionsMessagesReq) @@ -288,8 +288,8 @@ func TestChatHistoryReadState_HappyPath(t *testing.T) { Ts: timestamppb.Now(), Content: []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: fmt.Sprintf("msg.body.key%d", i), }, }, @@ -346,8 +346,8 @@ func TestChatHistoryReadState_NegativeProgress(t *testing.T) { Ts: timestamppb.Now(), Content: []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: fmt.Sprintf("msg.body.key%d", i), }, }, @@ -429,8 +429,8 @@ func TestChatHistoryReadState_MessageNotFound(t *testing.T) { Ts: timestamppb.Now(), Content: []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: "msg.body.key", }, }, @@ -743,8 +743,8 @@ func TestUnauthorizedAccess(t *testing.T) { Ts: timestamppb.Now(), Content: []*chatpb.Content{ { - Type: &chatpb.Content_Localized{ - Localized: &chatpb.LocalizedContent{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ KeyOrText: "msg.body.key", }, }, diff --git a/pkg/code/server/grpc/chat/stream.go b/pkg/code/server/grpc/chat/stream.go new file mode 100644 index 00000000..9d79969b --- /dev/null +++ b/pkg/code/server/grpc/chat/stream.go @@ -0,0 +1,121 @@ +package chat + +import ( + "context" + "sync" + "time" + + "github.com/pkg/errors" + "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" + + chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" +) + +const ( + // todo: configurable + streamBufferSize = 64 + streamPingDelay = 5 * time.Second + streamKeepAliveRecvTimeout = 10 * time.Second + streamNotifyTimeout = 10 * time.Second +) + +type chatEventStream struct { + sync.Mutex + + closed bool + streamCh chan *chatpb.ChatStreamEvent +} + +func newChatEventStream(bufferSize int) *chatEventStream { + return &chatEventStream{ + streamCh: make(chan *chatpb.ChatStreamEvent, bufferSize), + } +} + +func (s *chatEventStream) notify(event *chatpb.ChatStreamEvent, timeout time.Duration) error { + m := proto.Clone(event).(*chatpb.ChatStreamEvent) + + s.Lock() + + if s.closed { + s.Unlock() + return errors.New("cannot notify closed stream") + } + + select { + case s.streamCh <- m: + case <-time.After(timeout): + s.Unlock() + s.close() + return errors.New("timed out sending message to streamCh") + } + + s.Unlock() + return nil +} + +func (s *chatEventStream) close() { + s.Lock() + defer s.Unlock() + + if s.closed { + return + } + + s.closed = true + close(s.streamCh) +} + +func boundedStreamChatEventsRecv( + ctx context.Context, + streamer chatpb.Chat_StreamChatEventsServer, + timeout time.Duration, +) (req *chatpb.StreamChatEventsRequest, err error) { + done := make(chan struct{}) + go func() { + req, err = streamer.Recv() + close(done) + }() + + select { + case <-done: + return req, err + case <-ctx.Done(): + return nil, status.Error(codes.Canceled, "") + case <-time.After(timeout): + return nil, status.Error(codes.DeadlineExceeded, "timed out receiving message") + } +} + +// Very naive implementation to start +func monitorChatEventStreamHealth( + ctx context.Context, + log *logrus.Entry, + ssRef string, + streamer chatpb.Chat_StreamChatEventsServer, +) <-chan struct{} { + streamHealthChan := make(chan struct{}) + go func() { + defer close(streamHealthChan) + + for { + // todo: configurable timeout + req, err := boundedStreamChatEventsRecv(ctx, streamer, streamKeepAliveRecvTimeout) + if err != nil { + return + } + + switch req.Type.(type) { + case *chatpb.StreamChatEventsRequest_Pong: + log.Tracef("received pong from client (stream=%s)", ssRef) + default: + // Client sent something unexpected. Terminate the stream + return + } + } + }() + return streamHealthChan +} diff --git a/pkg/code/server/grpc/messaging/server.go b/pkg/code/server/grpc/messaging/server.go index c89f69ce..87a35c3e 100644 --- a/pkg/code/server/grpc/messaging/server.go +++ b/pkg/code/server/grpc/messaging/server.go @@ -17,19 +17,19 @@ import ( "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" + commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" messagingpb "github.com/code-payments/code-protobuf-api/generated/go/messaging/v1" "github.com/code-payments/code-server/pkg/cache" - "github.com/code-payments/code-server/pkg/grpc/client" - "github.com/code-payments/code-server/pkg/retry" - "github.com/code-payments/code-server/pkg/retry/backoff" - "github.com/code-payments/code-server/pkg/code/auth" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/messaging" "github.com/code-payments/code-server/pkg/code/data/rendezvous" "github.com/code-payments/code-server/pkg/code/thirdparty" + "github.com/code-payments/code-server/pkg/grpc/client" + "github.com/code-payments/code-server/pkg/retry" + "github.com/code-payments/code-server/pkg/retry/backoff" ) const ( @@ -285,7 +285,7 @@ func (s *server) OpenMessageStreamWithKeepAlive(streamer messagingpb.Messaging_O err := streamer.Send(&messagingpb.OpenMessageStreamWithKeepAliveResponse{ ResponseOrPing: &messagingpb.OpenMessageStreamWithKeepAliveResponse_Ping{ - Ping: &messagingpb.ServerPing{ + Ping: &commonpb.ServerPing{ Timestamp: timestamppb.Now(), PingDelay: durationpb.New(messageStreamPingDelay), }, diff --git a/pkg/code/server/grpc/messaging/testutil.go b/pkg/code/server/grpc/messaging/testutil.go index f1e7cf97..4968ab2a 100644 --- a/pkg/code/server/grpc/messaging/testutil.go +++ b/pkg/code/server/grpc/messaging/testutil.go @@ -373,7 +373,7 @@ func (c *clientEnv) receiveMessagesInRealTime(t *testing.T, rendezvousKey *commo case *messagingpb.OpenMessageStreamWithKeepAliveResponse_Ping: require.NoError(t, streamer.streamWithKeepAlives.Send(&messagingpb.OpenMessageStreamWithKeepAliveRequest{ RequestOrPong: &messagingpb.OpenMessageStreamWithKeepAliveRequest_Pong{ - Pong: &messagingpb.ClientPong{ + Pong: &commonpb.ClientPong{ Timestamp: timestamppb.Now(), }, }, @@ -467,7 +467,7 @@ func (c *clientEnv) waitUntilStreamTerminationOrTimeout(t *testing.T, rendezvous if keepStreamAlive { require.NoError(t, streamer.streamWithKeepAlives.Send(&messagingpb.OpenMessageStreamWithKeepAliveRequest{ RequestOrPong: &messagingpb.OpenMessageStreamWithKeepAliveRequest_Pong{ - Pong: &messagingpb.ClientPong{ + Pong: &commonpb.ClientPong{ Timestamp: timestamppb.Now(), }, }, diff --git a/pkg/code/server/grpc/transaction/v2/swap.go b/pkg/code/server/grpc/transaction/v2/swap.go index 137328df..792e3982 100644 --- a/pkg/code/server/grpc/transaction/v2/swap.go +++ b/pkg/code/server/grpc/transaction/v2/swap.go @@ -520,8 +520,8 @@ func (s *transactionServer) bestEffortNotifyUserOfSwapInProgress(ctx context.Con } switch typed := protoChatMessage.Content[0].Type.(type) { - case *chatpb.Content_Localized: - if typed.Localized.KeyOrText != localization.ChatMessageUsdcDeposited { + case *chatpb.Content_ServerLocalized: + if typed.ServerLocalized.KeyOrText != localization.ChatMessageUsdcDeposited { return nil } } From 413bc99a08a2a573f5e971181e5897d15accee16 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 17 May 2024 11:44:02 -0400 Subject: [PATCH 02/40] Move chat event stream notification into an async worker --- pkg/code/server/grpc/chat/server.go | 61 +++++++++++++---------------- pkg/code/server/grpc/chat/stream.go | 58 ++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 35 deletions(-) diff --git a/pkg/code/server/grpc/chat/server.go b/pkg/code/server/grpc/chat/server.go index b66ac9fe..d76dc50d 100644 --- a/pkg/code/server/grpc/chat/server.go +++ b/pkg/code/server/grpc/chat/server.go @@ -5,7 +5,6 @@ import ( "context" "fmt" "math" - "strings" "sync" "time" @@ -28,6 +27,7 @@ import ( "github.com/code-payments/code-server/pkg/code/localization" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/grpc/client" + sync_util "github.com/code-payments/code-server/pkg/sync" ) const ( @@ -47,16 +47,27 @@ type server struct { streamsMu sync.RWMutex streams map[string]*chatEventStream + chatLocks *sync_util.StripedLock + chatEventChans *sync_util.StripedChannel + chatpb.UnimplementedChatServer } func NewChatServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier) chatpb.ChatServer { - return &server{ - log: logrus.StandardLogger().WithField("type", "chat/server"), - data: data, - auth: auth, - streams: make(map[string]*chatEventStream), + s := &server{ + log: logrus.StandardLogger().WithField("type", "chat/server"), + data: data, + auth: auth, + streams: make(map[string]*chatEventStream), + chatLocks: sync_util.NewStripedLock(64), // todo: configurable parameters + chatEventChans: sync_util.NewStripedChannel(64, 100_000), // todo: configurable parameters + } + + for i, channel := range s.chatEventChans.GetChannels() { + go s.asyncChatEventStreamNotifier(i, channel) } + + return s } func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*chatpb.GetChatsResponse, error) { @@ -387,21 +398,9 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR Pointers: []*chatpb.Pointer{req.Pointer}, } - s.streamsMu.RLock() - for key, stream := range s.streams { - if !strings.HasPrefix(key, chatId.String()) { - continue - } - - if strings.HasSuffix(key, owner.PublicKey().ToBase58()) { - continue - } - - if err := stream.notify(event, streamNotifyTimeout); err != nil { - log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) - } + if err := s.asyncNotifyAll(chatId, owner, event); err != nil { + log.WithError(err).Warn("failure notifying chat event") } - s.streamsMu.RUnlock() return &chatpb.AdvancePointerResponse{ Result: chatpb.AdvancePointerResponse_OK, @@ -718,6 +717,10 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest return nil, status.Error(codes.InvalidArgument, "content[0] must be UserText") } + chatLock := s.chatLocks.Get(chatId[:]) + chatLock.Lock() + defer chatLock.Unlock() + // todo: Revisit message IDs messageId, err := common.NewRandomAccount() if err != nil { @@ -733,25 +736,15 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest Cursor: nil, // todo: Don't have cursor until we save it to the DB } + // todo: Save the message to the DB + event := &chatpb.ChatStreamEvent{ Messages: []*chatpb.ChatMessage{chatMessage}, } - s.streamsMu.RLock() - for key, stream := range s.streams { - if !strings.HasPrefix(key, chatId.String()) { - continue - } - - if strings.HasSuffix(key, owner.PublicKey().ToBase58()) { - continue - } - - if err := stream.notify(event, streamNotifyTimeout); err != nil { - log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) - } + if err := s.asyncNotifyAll(chatId, owner, event); err != nil { + log.WithError(err).Warn("failure notifying chat event") } - s.streamsMu.RUnlock() return &chatpb.SendMessageResponse{ Result: chatpb.SendMessageResponse_OK, diff --git a/pkg/code/server/grpc/chat/stream.go b/pkg/code/server/grpc/chat/stream.go index 9d79969b..d29bf7c8 100644 --- a/pkg/code/server/grpc/chat/stream.go +++ b/pkg/code/server/grpc/chat/stream.go @@ -2,6 +2,7 @@ package chat import ( "context" + "strings" "sync" "time" @@ -12,6 +13,8 @@ import ( "google.golang.org/protobuf/proto" chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" + "github.com/code-payments/code-server/pkg/code/common" + "github.com/code-payments/code-server/pkg/code/data/chat" ) const ( @@ -19,7 +22,7 @@ const ( streamBufferSize = 64 streamPingDelay = 5 * time.Second streamKeepAliveRecvTimeout = 10 * time.Second - streamNotifyTimeout = 10 * time.Second + streamNotifyTimeout = time.Second ) type chatEventStream struct { @@ -90,6 +93,59 @@ func boundedStreamChatEventsRecv( } } +type chatIdWithEvent struct { + chatId chat.ChatId + owner *common.Account + event *chatpb.ChatStreamEvent + ts time.Time +} + +func (s *server) asyncNotifyAll(chatId chat.ChatId, owner *common.Account, event *chatpb.ChatStreamEvent) error { + m := proto.Clone(event).(*chatpb.ChatStreamEvent) + ok := s.chatEventChans.Send(chatId[:], &chatIdWithEvent{chatId, owner, m, time.Now()}) + if !ok { + return errors.New("chat event channel is full") + } + return nil +} + +func (s *server) asyncChatEventStreamNotifier(workerId int, channel <-chan interface{}) { + log := s.log.WithFields(logrus.Fields{ + "method": "asyncChatEventStreamNotifier", + "worker": workerId, + }) + + for value := range channel { + typedValue, ok := value.(*chatIdWithEvent) + if !ok { + log.Warn("channel did not receive expected struct") + continue + } + + log := log.WithField("chat_id", typedValue.chatId.String()) + + if time.Since(typedValue.ts) > time.Second { + log.Warn("") + } + + s.streamsMu.RLock() + for key, stream := range s.streams { + if !strings.HasPrefix(key, typedValue.chatId.String()) { + continue + } + + if strings.HasSuffix(key, typedValue.owner.PublicKey().ToBase58()) { + continue + } + + if err := stream.notify(typedValue.event, streamNotifyTimeout); err != nil { + log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) + } + } + s.streamsMu.RUnlock() + } +} + // Very naive implementation to start func monitorChatEventStreamHealth( ctx context.Context, From b4ef04f48144ca56593d9dcedc291009f7470f75 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 17 May 2024 15:53:03 -0400 Subject: [PATCH 03/40] Ensure chat event streams are cleaned up after being closed --- pkg/code/server/grpc/chat/server.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pkg/code/server/grpc/chat/server.go b/pkg/code/server/grpc/chat/server.go index d76dc50d..2fe6d33f 100644 --- a/pkg/code/server/grpc/chat/server.go +++ b/pkg/code/server/grpc/chat/server.go @@ -639,6 +639,22 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e s.streamsMu.Unlock() + defer func() { + s.streamsMu.Lock() + + log.Tracef("closing streamer (stream=%s)", streamRef) + + // We check to see if the current active stream is the one that we created. + // If it is, we can just remove it since it's closed. Otherwise, we leave it + // be, as another OpenMessageStream() call is handling it. + liveStream, exists := s.streams[streamKey] + if exists && liveStream == stream { + delete(s.streams, streamKey) + } + + s.streamsMu.Unlock() + }() + sendPingCh := time.After(0) streamHealthCh := monitorChatEventStreamHealth(ctx, log, streamRef, streamer) From e645a76d8fe678c507e74c0b2eec9cf8e71fc637 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 21 May 2024 14:11:55 -0400 Subject: [PATCH 04/40] Add support for thank you messages --- go.mod | 2 +- go.sum | 4 ++-- pkg/code/server/grpc/chat/server.go | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index ce7a4ec7..5373429d 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.3-0.20240515163011-81178a652d87 + github.com/code-payments/code-protobuf-api v1.16.3-0.20240521175259-4b79f72c7b83 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index 8224ed64..a926491a 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.3-0.20240515163011-81178a652d87 h1:uKU2M+0A0x72AzWjJvH9L5iZohDOnhVeDKpE0da+vKc= -github.com/code-payments/code-protobuf-api v1.16.3-0.20240515163011-81178a652d87/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.16.3-0.20240521175259-4b79f72c7b83 h1:oSQmfOLDwFz2kKvwiipN3yPYemn9jx1y3tYuuqjbtmI= +github.com/code-payments/code-protobuf-api v1.16.3-0.20240521175259-4b79f72c7b83/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/pkg/code/server/grpc/chat/server.go b/pkg/code/server/grpc/chat/server.go index 2fe6d33f..9614c124 100644 --- a/pkg/code/server/grpc/chat/server.go +++ b/pkg/code/server/grpc/chat/server.go @@ -728,9 +728,9 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest } switch req.Content[0].Type.(type) { - case *chatpb.Content_UserText: + case *chatpb.Content_Text, *chatpb.Content_ThankYou: default: - return nil, status.Error(codes.InvalidArgument, "content[0] must be UserText") + return nil, status.Error(codes.InvalidArgument, "content[0] must be Text or ThankYou") } chatLock := s.chatLocks.Get(chatId[:]) From 69aba1b495f6d3598deb81dd210fb2708f1d7c25 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 21 May 2024 16:22:15 -0400 Subject: [PATCH 05/40] Rename struct --- pkg/code/server/grpc/chat/stream.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/code/server/grpc/chat/stream.go b/pkg/code/server/grpc/chat/stream.go index d29bf7c8..992970fb 100644 --- a/pkg/code/server/grpc/chat/stream.go +++ b/pkg/code/server/grpc/chat/stream.go @@ -93,7 +93,7 @@ func boundedStreamChatEventsRecv( } } -type chatIdWithEvent struct { +type chatEventNotification struct { chatId chat.ChatId owner *common.Account event *chatpb.ChatStreamEvent @@ -102,7 +102,7 @@ type chatIdWithEvent struct { func (s *server) asyncNotifyAll(chatId chat.ChatId, owner *common.Account, event *chatpb.ChatStreamEvent) error { m := proto.Clone(event).(*chatpb.ChatStreamEvent) - ok := s.chatEventChans.Send(chatId[:], &chatIdWithEvent{chatId, owner, m, time.Now()}) + ok := s.chatEventChans.Send(chatId[:], &chatEventNotification{chatId, owner, m, time.Now()}) if !ok { return errors.New("chat event channel is full") } @@ -116,7 +116,7 @@ func (s *server) asyncChatEventStreamNotifier(workerId int, channel <-chan inter }) for value := range channel { - typedValue, ok := value.(*chatIdWithEvent) + typedValue, ok := value.(*chatEventNotification) if !ok { log.Warn("channel did not receive expected struct") continue From 368854ed2ca05d79c60b1a2db68c570487ba4771 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 21 May 2024 16:36:49 -0400 Subject: [PATCH 06/40] Fill out missing log message --- pkg/code/server/grpc/chat/stream.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/code/server/grpc/chat/stream.go b/pkg/code/server/grpc/chat/stream.go index 992970fb..3f6ca6fa 100644 --- a/pkg/code/server/grpc/chat/stream.go +++ b/pkg/code/server/grpc/chat/stream.go @@ -125,7 +125,7 @@ func (s *server) asyncChatEventStreamNotifier(workerId int, channel <-chan inter log := log.WithField("chat_id", typedValue.chatId.String()) if time.Since(typedValue.ts) > time.Second { - log.Warn("") + log.Warn("channel notification latency is elevated") } s.streamsMu.RLock() From 73bb70464b1c61d4b43748fbbde695ba4a5e7e62 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 22 May 2024 10:32:33 -0400 Subject: [PATCH 07/40] Add basic push support for user messages --- pkg/code/push/notifications.go | 11 +++++- pkg/code/server/grpc/chat/server.go | 47 ++++++++++++++++++++++-- pkg/code/server/grpc/chat/server_test.go | 3 +- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/pkg/code/push/notifications.go b/pkg/code/push/notifications.go index 42aff9be..2635dddd 100644 --- a/pkg/code/push/notifications.go +++ b/pkg/code/push/notifications.go @@ -364,8 +364,17 @@ func SendChatMessagePushNotification( }, }, } - case *chatpb.Content_NaclBox: + case *chatpb.Content_NaclBox, *chatpb.Content_Text: contentToPush = content + case *chatpb.Content_ThankYou: + contentToPush = &chatpb.Content{ + Type: &chatpb.Content_ServerLocalized{ + ServerLocalized: &chatpb.ServerLocalizedContent{ + // todo: localize this + KeyOrText: "🙏 They thanked you for their tip", + }, + }, + } } if contentToPush == nil { diff --git a/pkg/code/server/grpc/chat/server.go b/pkg/code/server/grpc/chat/server.go index 9614c124..d27673de 100644 --- a/pkg/code/server/grpc/chat/server.go +++ b/pkg/code/server/grpc/chat/server.go @@ -5,6 +5,7 @@ import ( "context" "fmt" "math" + "strings" "sync" "time" @@ -25,8 +26,10 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/chat" "github.com/code-payments/code-server/pkg/code/localization" + push_util "github.com/code-payments/code-server/pkg/code/push" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/grpc/client" + push_lib "github.com/code-payments/code-server/pkg/push" sync_util "github.com/code-payments/code-server/pkg/sync" ) @@ -40,9 +43,10 @@ var ( // todo: Resolve duplication of streaming logic with messaging service. The latest and greatest will live here. type server struct { - log *logrus.Entry - data code_data.Provider - auth *auth_util.RPCSignatureVerifier + log *logrus.Entry + data code_data.Provider + auth *auth_util.RPCSignatureVerifier + pusher push_lib.Provider streamsMu sync.RWMutex streams map[string]*chatEventStream @@ -53,11 +57,12 @@ type server struct { chatpb.UnimplementedChatServer } -func NewChatServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier) chatpb.ChatServer { +func NewChatServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier, pusher push_lib.Provider) chatpb.ChatServer { s := &server{ log: logrus.StandardLogger().WithField("type", "chat/server"), data: data, auth: auth, + pusher: pusher, streams: make(map[string]*chatEventStream), chatLocks: sync_util.NewStripedLock(64), // todo: configurable parameters chatEventChans: sync_util.NewStripedChannel(64, 100_000), // todo: configurable parameters @@ -762,8 +767,42 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest log.WithError(err).Warn("failure notifying chat event") } + s.asyncPushChatMessage(ctx, owner, chatId, chatMessage) + return &chatpb.SendMessageResponse{ Result: chatpb.SendMessageResponse_OK, Message: chatMessage, }, nil } + +// todo: doesn't respect mute/unsubscribe rules +// todo: only sends pushes to active stream listeners instead of all message recipients +func (s *server) asyncPushChatMessage(ctx context.Context, sender *common.Account, chatId chat.ChatId, chatMessage *chatpb.ChatMessage) { + go func() { + s.streamsMu.RLock() + for key := range s.streams { + if !strings.HasPrefix(key, chatId.String()) { + continue + } + + receiver, err := common.NewAccountFromPublicKeyString(strings.Split(key, ":")[1]) + if err != nil { + continue + } + + if bytes.Equal(sender.PublicKey().ToBytes(), receiver.PublicKey().ToBytes()) { + continue + } + + go push_util.SendChatMessagePushNotification( + ctx, + s.data, + s.pusher, + "TontonTwitch", + receiver, + chatMessage, + ) + } + s.streamsMu.RUnlock() + }() +} diff --git a/pkg/code/server/grpc/chat/server_test.go b/pkg/code/server/grpc/chat/server_test.go index 627764b0..54af7c84 100644 --- a/pkg/code/server/grpc/chat/server_test.go +++ b/pkg/code/server/grpc/chat/server_test.go @@ -29,6 +29,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/user/storage" "github.com/code-payments/code-server/pkg/code/localization" "github.com/code-payments/code-server/pkg/kin" + memory_push "github.com/code-payments/code-server/pkg/push/memory" "github.com/code-payments/code-server/pkg/testutil" ) @@ -880,7 +881,7 @@ func setup(t *testing.T) (env *testEnv, cleanup func()) { data: code_data.NewTestDataProvider(), } - s := NewChatServer(env.data, auth_util.NewRPCSignatureVerifier(env.data)) + s := NewChatServer(env.data, auth_util.NewRPCSignatureVerifier(env.data), memory_push.NewPushProvider()) env.server = s.(*server) serv.RegisterService(func(server *grpc.Server) { From 51ea27c915532bc56b4a65ef5c4becea806128a6 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 22 May 2024 10:39:59 -0400 Subject: [PATCH 08/40] Use separate context for pushing user chat messages --- pkg/code/server/grpc/chat/server.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pkg/code/server/grpc/chat/server.go b/pkg/code/server/grpc/chat/server.go index d27673de..b2b9c58f 100644 --- a/pkg/code/server/grpc/chat/server.go +++ b/pkg/code/server/grpc/chat/server.go @@ -767,7 +767,7 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest log.WithError(err).Warn("failure notifying chat event") } - s.asyncPushChatMessage(ctx, owner, chatId, chatMessage) + s.asyncPushChatMessage(owner, chatId, chatMessage) return &chatpb.SendMessageResponse{ Result: chatpb.SendMessageResponse_OK, @@ -777,7 +777,9 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest // todo: doesn't respect mute/unsubscribe rules // todo: only sends pushes to active stream listeners instead of all message recipients -func (s *server) asyncPushChatMessage(ctx context.Context, sender *common.Account, chatId chat.ChatId, chatMessage *chatpb.ChatMessage) { +func (s *server) asyncPushChatMessage(sender *common.Account, chatId chat.ChatId, chatMessage *chatpb.ChatMessage) { + ctx := context.TODO() + go func() { s.streamsMu.RLock() for key := range s.streams { From 310dc3f5ee28329bb5acd94a09e61a649f9172f9 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 27 May 2024 09:11:15 -0400 Subject: [PATCH 09/40] Fix build --- pkg/code/server/grpc/transaction/v2/errors.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/code/server/grpc/transaction/v2/errors.go b/pkg/code/server/grpc/transaction/v2/errors.go index 4b078e15..d806a3f6 100644 --- a/pkg/code/server/grpc/transaction/v2/errors.go +++ b/pkg/code/server/grpc/transaction/v2/errors.go @@ -226,7 +226,7 @@ func toDeniedErrorDetails(err error) *transactionpb.ErrorDetails { case antispam.ReasonTooManyFreeAccountsForPhoneNumber: code = transactionpb.DeniedErrorDetails_TOO_MANY_FREE_ACCOUNTS_FOR_PHONE_NUMBER case antispam.ReasonTooManyFreeAccountsForDevice: - code = transactionpb.DeniedErrorDetails_TOO_MANY_FREE_ACCOUNTS_FOR_DEVIEC + code = transactionpb.DeniedErrorDetails_TOO_MANY_FREE_ACCOUNTS_FOR_DEVICE default: code = transactionpb.DeniedErrorDetails_UNSPECIFIED } From 67928e6bead66c26f375b46f5f6d421f7457d740 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 7 Jun 2024 10:12:26 -0400 Subject: [PATCH 10/40] Move existing chat stuff to v1 in prep for v2 --- pkg/code/async/geyser/external_deposit.go | 6 +- pkg/code/async/geyser/messenger.go | 6 +- pkg/code/chat/message_cash_transactions.go | 10 +-- pkg/code/chat/message_code_team.go | 4 +- pkg/code/chat/message_kin_purchases.go | 8 +- pkg/code/chat/message_merchant.go | 14 ++-- pkg/code/chat/message_tips.go | 6 +- pkg/code/chat/sender.go | 20 ++--- pkg/code/chat/sender_test.go | 54 ++++++------ pkg/code/data/chat/{ => v1}/memory/store.go | 2 +- .../data/chat/{ => v1}/memory/store_test.go | 2 +- pkg/code/data/chat/{ => v1}/model.go | 2 +- pkg/code/data/chat/{ => v1}/model_test.go | 2 +- pkg/code/data/chat/{ => v1}/postgres/model.go | 2 +- pkg/code/data/chat/{ => v1}/postgres/store.go | 2 +- .../data/chat/{ => v1}/postgres/store_test.go | 4 +- pkg/code/data/chat/{ => v1}/store.go | 2 +- pkg/code/data/chat/{ => v1}/tests/tests.go | 2 +- pkg/code/data/internal.go | 82 +++++++++---------- pkg/code/push/notifications.go | 10 +-- pkg/code/server/grpc/chat/{ => v1}/server.go | 30 +++---- .../server/grpc/chat/{ => v1}/server_test.go | 4 +- pkg/code/server/grpc/chat/{ => v1}/stream.go | 4 +- .../grpc/transaction/v2/history_test.go | 16 ++-- pkg/code/server/grpc/transaction/v2/swap.go | 6 +- .../server/grpc/transaction/v2/testutil.go | 4 +- 26 files changed, 152 insertions(+), 152 deletions(-) rename pkg/code/data/chat/{ => v1}/memory/store.go (99%) rename pkg/code/data/chat/{ => v1}/memory/store_test.go (74%) rename pkg/code/data/chat/{ => v1}/model.go (99%) rename pkg/code/data/chat/{ => v1}/model_test.go (97%) rename pkg/code/data/chat/{ => v1}/postgres/model.go (99%) rename pkg/code/data/chat/{ => v1}/postgres/store.go (98%) rename pkg/code/data/chat/{ => v1}/postgres/store_test.go (95%) rename pkg/code/data/chat/{ => v1}/store.go (99%) rename pkg/code/data/chat/{ => v1}/tests/tests.go (99%) rename pkg/code/server/grpc/chat/{ => v1}/server.go (96%) rename pkg/code/server/grpc/chat/{ => v1}/server_test.go (99%) rename pkg/code/server/grpc/chat/{ => v1}/stream.go (97%) diff --git a/pkg/code/async/geyser/external_deposit.go b/pkg/code/async/geyser/external_deposit.go index 1b5939ac..48685818 100644 --- a/pkg/code/async/geyser/external_deposit.go +++ b/pkg/code/async/geyser/external_deposit.go @@ -20,7 +20,7 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/balance" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/data/deposit" "github.com/code-payments/code-server/pkg/code/data/fulfillment" "github.com/code-payments/code-server/pkg/code/data/intent" @@ -299,7 +299,7 @@ func processPotentialExternalDeposit(ctx context.Context, conf *conf, data code_ chatMessage, ) } - case chat.ErrMessageAlreadyExists: + case chat_v1.ErrMessageAlreadyExists: default: return errors.Wrap(err, "error sending chat message") } @@ -772,7 +772,7 @@ func delayedUsdcDepositProcessing( chatMessage, ) } - case chat.ErrMessageAlreadyExists: + case chat_v1.ErrMessageAlreadyExists: default: return } diff --git a/pkg/code/async/geyser/messenger.go b/pkg/code/async/geyser/messenger.go index 5c94153c..afb8e999 100644 --- a/pkg/code/async/geyser/messenger.go +++ b/pkg/code/async/geyser/messenger.go @@ -15,7 +15,7 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/push" "github.com/code-payments/code-server/pkg/code/thirdparty" "github.com/code-payments/code-server/pkg/database/query" @@ -169,13 +169,13 @@ func processPotentialBlockchainMessage(ctx context.Context, data code_data.Provi ctx, data, asciiBaseDomain, - chat.ChatTypeExternalApp, + chat_v1.ChatTypeExternalApp, true, recipientOwner, chatMessage, false, ) - if err != nil && err != chat.ErrMessageAlreadyExists { + if err != nil && err != chat_v1.ErrMessageAlreadyExists { return errors.Wrap(err, "error persisting chat message") } diff --git a/pkg/code/chat/message_cash_transactions.go b/pkg/code/chat/message_cash_transactions.go index 1976d9d5..8a94361f 100644 --- a/pkg/code/chat/message_cash_transactions.go +++ b/pkg/code/chat/message_cash_transactions.go @@ -11,7 +11,7 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/data/intent" ) @@ -93,9 +93,9 @@ func SendCashTransactionsExchangeMessage(ctx context.Context, data code_data.Pro return errors.Wrap(err, "error getting original gift card issued intent") } - chatId := chat.GetChatId(CashTransactionsName, giftCardIssuedIntentRecord.InitiatorOwnerAccount, true) + chatId := chat_v1.GetChatId(CashTransactionsName, giftCardIssuedIntentRecord.InitiatorOwnerAccount, true) - err = data.DeleteChatMessage(ctx, chatId, giftCardIssuedIntentRecord.IntentId) + err = data.DeleteChatMessageV1(ctx, chatId, giftCardIssuedIntentRecord.IntentId) if err != nil { return errors.Wrap(err, "error deleting chat message") } @@ -152,13 +152,13 @@ func SendCashTransactionsExchangeMessage(ctx context.Context, data code_data.Pro ctx, data, CashTransactionsName, - chat.ChatTypeInternal, + chat_v1.ChatTypeInternal, true, receiver, protoMessage, true, ) - if err != nil && err != chat.ErrMessageAlreadyExists { + if err != nil && err != chat_v1.ErrMessageAlreadyExists { return errors.Wrap(err, "error persisting chat message") } } diff --git a/pkg/code/chat/message_code_team.go b/pkg/code/chat/message_code_team.go index 536370a3..d24e2305 100644 --- a/pkg/code/chat/message_code_team.go +++ b/pkg/code/chat/message_code_team.go @@ -9,7 +9,7 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/localization" ) @@ -20,7 +20,7 @@ func SendCodeTeamMessage(ctx context.Context, data code_data.Provider, receiver ctx, data, CodeTeamName, - chat.ChatTypeInternal, + chat_v1.ChatTypeInternal, true, receiver, chatMessage, diff --git a/pkg/code/chat/message_kin_purchases.go b/pkg/code/chat/message_kin_purchases.go index f9a2c6fd..4377c247 100644 --- a/pkg/code/chat/message_kin_purchases.go +++ b/pkg/code/chat/message_kin_purchases.go @@ -11,14 +11,14 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/localization" ) // GetKinPurchasesChatId returns the chat ID for the Kin Purchases chat for a // given owner account -func GetKinPurchasesChatId(owner *common.Account) chat.ChatId { - return chat.GetChatId(KinPurchasesName, owner.PublicKey().ToBase58(), true) +func GetKinPurchasesChatId(owner *common.Account) chat_v1.ChatId { + return chat_v1.GetChatId(KinPurchasesName, owner.PublicKey().ToBase58(), true) } // SendKinPurchasesMessage sends a message to the Kin Purchases chat. @@ -27,7 +27,7 @@ func SendKinPurchasesMessage(ctx context.Context, data code_data.Provider, recei ctx, data, KinPurchasesName, - chat.ChatTypeInternal, + chat_v1.ChatTypeInternal, true, receiver, chatMessage, diff --git a/pkg/code/chat/message_merchant.go b/pkg/code/chat/message_merchant.go index b4504c39..a340f8bd 100644 --- a/pkg/code/chat/message_merchant.go +++ b/pkg/code/chat/message_merchant.go @@ -13,7 +13,7 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/data/intent" ) @@ -36,7 +36,7 @@ func SendMerchantExchangeMessage(ctx context.Context, data code_data.Provider, i // the merchant. Representation in the UI may differ (ie. 2 and 3 are grouped), // but this is the most flexible solution with the chat model. chatTitle := PaymentsName - chatType := chat.ChatTypeInternal + chatType := chat_v1.ChatTypeInternal isVerifiedChat := false exchangeData, ok := getExchangeDataFromIntent(intentRecord) @@ -59,7 +59,7 @@ func SendMerchantExchangeMessage(ctx context.Context, data code_data.Provider, i if paymentRequestRecord.Domain != nil { chatTitle = *paymentRequestRecord.Domain - chatType = chat.ChatTypeExternalApp + chatType = chat_v1.ChatTypeExternalApp isVerifiedChat = paymentRequestRecord.IsVerified } @@ -87,7 +87,7 @@ func SendMerchantExchangeMessage(ctx context.Context, data code_data.Provider, i // and will have merchant payments appear in the verified merchant // chat. chatTitle = *destinationAccountInfoRecord.RelationshipTo - chatType = chat.ChatTypeExternalApp + chatType = chat_v1.ChatTypeExternalApp isVerifiedChat = true verbAndExchangeDataByMessageReceiver[intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount] = &verbAndExchangeData{ verb: chatpb.ExchangeDataContent_RECEIVED, @@ -107,7 +107,7 @@ func SendMerchantExchangeMessage(ctx context.Context, data code_data.Provider, i // and will have merchant payments appear in the verified merchant // chat. chatTitle = *destinationAccountInfoRecord.RelationshipTo - chatType = chat.ChatTypeExternalApp + chatType = chat_v1.ChatTypeExternalApp isVerifiedChat = true verbAndExchangeDataByMessageReceiver[intentRecord.SendPublicPaymentMetadata.DestinationOwnerAccount] = &verbAndExchangeData{ verb: chatpb.ExchangeDataContent_RECEIVED, @@ -126,7 +126,7 @@ func SendMerchantExchangeMessage(ctx context.Context, data code_data.Provider, i // and will have merchant payments appear in the verified merchant // chat. chatTitle = *destinationAccountInfoRecord.RelationshipTo - chatType = chat.ChatTypeExternalApp + chatType = chat_v1.ChatTypeExternalApp isVerifiedChat = true verbAndExchangeDataByMessageReceiver[intentRecord.ExternalDepositMetadata.DestinationOwnerAccount] = &verbAndExchangeData{ verb: chatpb.ExchangeDataContent_RECEIVED, @@ -171,7 +171,7 @@ func SendMerchantExchangeMessage(ctx context.Context, data code_data.Provider, i protoMessage, verbAndExchangeData.verb != chatpb.ExchangeDataContent_RECEIVED || !isVerifiedChat, ) - if err != nil && err != chat.ErrMessageAlreadyExists { + if err != nil && err != chat_v1.ErrMessageAlreadyExists { return nil, errors.Wrap(err, "error persisting chat message") } diff --git a/pkg/code/chat/message_tips.go b/pkg/code/chat/message_tips.go index b9984a9b..5752205a 100644 --- a/pkg/code/chat/message_tips.go +++ b/pkg/code/chat/message_tips.go @@ -9,7 +9,7 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/data/intent" ) @@ -70,13 +70,13 @@ func SendTipsExchangeMessage(ctx context.Context, data code_data.Provider, inten ctx, data, TipsName, - chat.ChatTypeInternal, + chat_v1.ChatTypeInternal, true, receiver, protoMessage, verb != chatpb.ExchangeDataContent_RECEIVED_TIP, ) - if err != nil && err != chat.ErrMessageAlreadyExists { + if err != nil && err != chat_v1.ErrMessageAlreadyExists { return nil, errors.Wrap(err, "error persisting chat message") } diff --git a/pkg/code/chat/sender.go b/pkg/code/chat/sender.go index 41da0902..412e656b 100644 --- a/pkg/code/chat/sender.go +++ b/pkg/code/chat/sender.go @@ -12,7 +12,7 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" ) // SendChatMessage sends a chat message to a receiving owner account. @@ -24,13 +24,13 @@ func SendChatMessage( ctx context.Context, data code_data.Provider, chatTitle string, - chatType chat.ChatType, + chatType chat_v1.ChatType, isVerifiedChat bool, receiver *common.Account, protoMessage *chatpb.ChatMessage, isSilentMessage bool, ) (canPushMessage bool, err error) { - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), isVerifiedChat) + chatId := chat_v1.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), isVerifiedChat) if protoMessage.Cursor != nil { // Let the utilities and GetMessages RPC handle cursors @@ -58,13 +58,13 @@ func SendChatMessage( canPersistMessage := true canPushMessage = !isSilentMessage - existingChatRecord, err := data.GetChatById(ctx, chatId) + existingChatRecord, err := data.GetChatByIdV1(ctx, chatId) switch err { case nil: canPersistMessage = !existingChatRecord.IsUnsubscribed canPushMessage = canPushMessage && canPersistMessage && !existingChatRecord.IsMuted - case chat.ErrChatNotFound: - chatRecord := &chat.Chat{ + case chat_v1.ErrChatNotFound: + chatRecord := &chat_v1.Chat{ ChatId: chatId, ChatType: chatType, ChatTitle: chatTitle, @@ -79,8 +79,8 @@ func SendChatMessage( CreatedAt: time.Now(), } - err = data.PutChat(ctx, chatRecord) - if err != nil && err != chat.ErrChatAlreadyExists { + err = data.PutChatV1(ctx, chatRecord) + if err != nil && err != chat_v1.ErrChatAlreadyExists { return false, err } default: @@ -88,7 +88,7 @@ func SendChatMessage( } if canPersistMessage { - messageRecord := &chat.Message{ + messageRecord := &chat_v1.Message{ ChatId: chatId, MessageId: base58.Encode(messageId), @@ -100,7 +100,7 @@ func SendChatMessage( Timestamp: ts.AsTime(), } - err = data.PutChatMessage(ctx, messageRecord) + err = data.PutChatMessageV1(ctx, messageRecord) if err != nil { return false, err } diff --git a/pkg/code/chat/sender_test.go b/pkg/code/chat/sender_test.go index 3438b3c8..ac767fd9 100644 --- a/pkg/code/chat/sender_test.go +++ b/pkg/code/chat/sender_test.go @@ -17,7 +17,7 @@ import ( "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/badgecount" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/testutil" ) @@ -26,14 +26,14 @@ func TestSendChatMessage_HappyPath(t *testing.T) { chatTitle := CodeTeamName receiver := testutil.NewRandomAccount(t) - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) + chatId := chat_v1.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) var expectedBadgeCount int for i := 0; i < 10; i++ { chatMessage := newRandomChatMessage(t, i+1) expectedBadgeCount += 1 - canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, true, receiver, chatMessage, false) + canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false) require.NoError(t, err) assert.True(t, canPush) @@ -56,7 +56,7 @@ func TestSendChatMessage_VerifiedChat(t *testing.T) { for _, isVerified := range []bool{true, false} { chatMessage := newRandomChatMessage(t, 1) - _, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, isVerified, receiver, chatMessage, true) + _, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, isVerified, receiver, chatMessage, true) require.NoError(t, err) env.assertChatRecordSaved(t, chatTitle, receiver, isVerified) } @@ -67,11 +67,11 @@ func TestSendChatMessage_SilentMessage(t *testing.T) { chatTitle := CodeTeamName receiver := testutil.NewRandomAccount(t) - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) + chatId := chat_v1.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) for i, isSilent := range []bool{true, false} { chatMessage := newRandomChatMessage(t, 1) - canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, true, receiver, chatMessage, isSilent) + canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, isSilent) require.NoError(t, err) assert.Equal(t, !isSilent, canPush) env.assertChatMessageRecordSaved(t, chatId, chatMessage, isSilent) @@ -84,7 +84,7 @@ func TestSendChatMessage_MuteState(t *testing.T) { chatTitle := CodeTeamName receiver := testutil.NewRandomAccount(t) - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) + chatId := chat_v1.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) for _, isMuted := range []bool{false, true} { if isMuted { @@ -92,7 +92,7 @@ func TestSendChatMessage_MuteState(t *testing.T) { } chatMessage := newRandomChatMessage(t, 1) - canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, true, receiver, chatMessage, false) + canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false) require.NoError(t, err) assert.Equal(t, !isMuted, canPush) env.assertChatMessageRecordSaved(t, chatId, chatMessage, false) @@ -105,7 +105,7 @@ func TestSendChatMessage_SubscriptionState(t *testing.T) { chatTitle := CodeTeamName receiver := testutil.NewRandomAccount(t) - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) + chatId := chat_v1.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) for _, isUnsubscribed := range []bool{false, true} { if isUnsubscribed { @@ -113,7 +113,7 @@ func TestSendChatMessage_SubscriptionState(t *testing.T) { } chatMessage := newRandomChatMessage(t, 1) - canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, true, receiver, chatMessage, false) + canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false) require.NoError(t, err) assert.Equal(t, !isUnsubscribed, canPush) if isUnsubscribed { @@ -130,12 +130,12 @@ func TestSendChatMessage_InvalidProtoMessage(t *testing.T) { chatTitle := CodeTeamName receiver := testutil.NewRandomAccount(t) - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) + chatId := chat_v1.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), true) chatMessage := newRandomChatMessage(t, 1) chatMessage.Content = nil - canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat.ChatTypeInternal, true, receiver, chatMessage, false) + canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false) assert.Error(t, err) assert.False(t, canPush) env.assertChatRecordNotSaved(t, chatId) @@ -173,13 +173,13 @@ func newRandomChatMessage(t *testing.T, contentLength int) *chatpb.ChatMessage { } func (e *testEnv) assertChatRecordSaved(t *testing.T, chatTitle string, receiver *common.Account, isVerified bool) { - chatId := chat.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), isVerified) + chatId := chat_v1.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), isVerified) - chatRecord, err := e.data.GetChatById(e.ctx, chatId) + chatRecord, err := e.data.GetChatByIdV1(e.ctx, chatId) require.NoError(t, err) assert.Equal(t, chatId[:], chatRecord.ChatId[:]) - assert.Equal(t, chat.ChatTypeInternal, chatRecord.ChatType) + assert.Equal(t, chat_v1.ChatTypeInternal, chatRecord.ChatType) assert.Equal(t, chatTitle, chatRecord.ChatTitle) assert.Equal(t, isVerified, chatRecord.IsVerified) assert.Equal(t, receiver.PublicKey().ToBase58(), chatRecord.CodeUser) @@ -188,8 +188,8 @@ func (e *testEnv) assertChatRecordSaved(t *testing.T, chatTitle string, receiver assert.False(t, chatRecord.IsUnsubscribed) } -func (e *testEnv) assertChatMessageRecordSaved(t *testing.T, chatId chat.ChatId, protoMessage *chatpb.ChatMessage, isSilent bool) { - messageRecord, err := e.data.GetChatMessage(e.ctx, chatId, base58.Encode(protoMessage.GetMessageId().Value)) +func (e *testEnv) assertChatMessageRecordSaved(t *testing.T, chatId chat_v1.ChatId, protoMessage *chatpb.ChatMessage, isSilent bool) { + messageRecord, err := e.data.GetChatMessageV1(e.ctx, chatId, base58.Encode(protoMessage.GetMessageId().Value)) require.NoError(t, err) cloned := proto.Clone(protoMessage).(*chatpb.ChatMessage) @@ -218,22 +218,22 @@ func (e *testEnv) assertBadgeCount(t *testing.T, owner *common.Account, expected assert.EqualValues(t, expected, badgeCountRecord.BadgeCount) } -func (e *testEnv) assertChatRecordNotSaved(t *testing.T, chatId chat.ChatId) { - _, err := e.data.GetChatById(e.ctx, chatId) - assert.Equal(t, chat.ErrChatNotFound, err) +func (e *testEnv) assertChatRecordNotSaved(t *testing.T, chatId chat_v1.ChatId) { + _, err := e.data.GetChatByIdV1(e.ctx, chatId) + assert.Equal(t, chat_v1.ErrChatNotFound, err) } -func (e *testEnv) assertChatMessageRecordNotSaved(t *testing.T, chatId chat.ChatId, messageId *chatpb.ChatMessageId) { - _, err := e.data.GetChatMessage(e.ctx, chatId, base58.Encode(messageId.Value)) - assert.Equal(t, chat.ErrMessageNotFound, err) +func (e *testEnv) assertChatMessageRecordNotSaved(t *testing.T, chatId chat_v1.ChatId, messageId *chatpb.ChatMessageId) { + _, err := e.data.GetChatMessageV1(e.ctx, chatId, base58.Encode(messageId.Value)) + assert.Equal(t, chat_v1.ErrMessageNotFound, err) } -func (e *testEnv) muteChat(t *testing.T, chatId chat.ChatId) { - require.NoError(t, e.data.SetChatMuteState(e.ctx, chatId, true)) +func (e *testEnv) muteChat(t *testing.T, chatId chat_v1.ChatId) { + require.NoError(t, e.data.SetChatMuteStateV1(e.ctx, chatId, true)) } -func (e *testEnv) unsubscribeFromChat(t *testing.T, chatId chat.ChatId) { - require.NoError(t, e.data.SetChatSubscriptionState(e.ctx, chatId, false)) +func (e *testEnv) unsubscribeFromChat(t *testing.T, chatId chat_v1.ChatId) { + require.NoError(t, e.data.SetChatSubscriptionStateV1(e.ctx, chatId, false)) } diff --git a/pkg/code/data/chat/memory/store.go b/pkg/code/data/chat/v1/memory/store.go similarity index 99% rename from pkg/code/data/chat/memory/store.go rename to pkg/code/data/chat/v1/memory/store.go index 1b869007..1d923071 100644 --- a/pkg/code/data/chat/memory/store.go +++ b/pkg/code/data/chat/v1/memory/store.go @@ -7,8 +7,8 @@ import ( "sync" "time" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/chat" ) type ChatsById []*chat.Chat diff --git a/pkg/code/data/chat/memory/store_test.go b/pkg/code/data/chat/v1/memory/store_test.go similarity index 74% rename from pkg/code/data/chat/memory/store_test.go rename to pkg/code/data/chat/v1/memory/store_test.go index 5d2c18a5..c27859e6 100644 --- a/pkg/code/data/chat/memory/store_test.go +++ b/pkg/code/data/chat/v1/memory/store_test.go @@ -3,7 +3,7 @@ package memory import ( "testing" - "github.com/code-payments/code-server/pkg/code/data/chat/tests" + "github.com/code-payments/code-server/pkg/code/data/chat/v1/tests" ) func TestChatMemoryStore(t *testing.T) { diff --git a/pkg/code/data/chat/model.go b/pkg/code/data/chat/v1/model.go similarity index 99% rename from pkg/code/data/chat/model.go rename to pkg/code/data/chat/v1/model.go index d8fe7432..4d156996 100644 --- a/pkg/code/data/chat/model.go +++ b/pkg/code/data/chat/v1/model.go @@ -1,4 +1,4 @@ -package chat +package chat_v1 import ( "bytes" diff --git a/pkg/code/data/chat/model_test.go b/pkg/code/data/chat/v1/model_test.go similarity index 97% rename from pkg/code/data/chat/model_test.go rename to pkg/code/data/chat/v1/model_test.go index 7774d286..062f372b 100644 --- a/pkg/code/data/chat/model_test.go +++ b/pkg/code/data/chat/v1/model_test.go @@ -1,4 +1,4 @@ -package chat +package chat_v1 import ( "testing" diff --git a/pkg/code/data/chat/postgres/model.go b/pkg/code/data/chat/v1/postgres/model.go similarity index 99% rename from pkg/code/data/chat/postgres/model.go rename to pkg/code/data/chat/v1/postgres/model.go index 07158095..8987df2b 100644 --- a/pkg/code/data/chat/postgres/model.go +++ b/pkg/code/data/chat/v1/postgres/model.go @@ -8,10 +8,10 @@ import ( "github.com/jmoiron/sqlx" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v1" pgutil "github.com/code-payments/code-server/pkg/database/postgres" q "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/chat" ) const ( diff --git a/pkg/code/data/chat/postgres/store.go b/pkg/code/data/chat/v1/postgres/store.go similarity index 98% rename from pkg/code/data/chat/postgres/store.go rename to pkg/code/data/chat/v1/postgres/store.go index 943a1935..bfb2b14f 100644 --- a/pkg/code/data/chat/postgres/store.go +++ b/pkg/code/data/chat/v1/postgres/store.go @@ -6,8 +6,8 @@ import ( "github.com/jmoiron/sqlx" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/database/query" - "github.com/code-payments/code-server/pkg/code/data/chat" ) type store struct { diff --git a/pkg/code/data/chat/postgres/store_test.go b/pkg/code/data/chat/v1/postgres/store_test.go similarity index 95% rename from pkg/code/data/chat/postgres/store_test.go rename to pkg/code/data/chat/v1/postgres/store_test.go index 49143ad7..4d72fc4e 100644 --- a/pkg/code/data/chat/postgres/store_test.go +++ b/pkg/code/data/chat/v1/postgres/store_test.go @@ -8,8 +8,8 @@ import ( "github.com/ory/dockertest/v3" "github.com/sirupsen/logrus" - "github.com/code-payments/code-server/pkg/code/data/chat" - "github.com/code-payments/code-server/pkg/code/data/chat/tests" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v1" + "github.com/code-payments/code-server/pkg/code/data/chat/v1/tests" postgrestest "github.com/code-payments/code-server/pkg/database/postgres/test" diff --git a/pkg/code/data/chat/store.go b/pkg/code/data/chat/v1/store.go similarity index 99% rename from pkg/code/data/chat/store.go rename to pkg/code/data/chat/v1/store.go index 2e79a228..c21471b5 100644 --- a/pkg/code/data/chat/store.go +++ b/pkg/code/data/chat/v1/store.go @@ -1,4 +1,4 @@ -package chat +package chat_v1 import ( "context" diff --git a/pkg/code/data/chat/tests/tests.go b/pkg/code/data/chat/v1/tests/tests.go similarity index 99% rename from pkg/code/data/chat/tests/tests.go rename to pkg/code/data/chat/v1/tests/tests.go index f9eaaadf..1453c133 100644 --- a/pkg/code/data/chat/tests/tests.go +++ b/pkg/code/data/chat/v1/tests/tests.go @@ -10,9 +10,9 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/pointer" - "github.com/code-payments/code-server/pkg/code/data/chat" ) func RunTests(t *testing.T, s chat.Store, teardown func()) { diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 49479c84..95ce26f2 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -25,7 +25,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/airdrop" "github.com/code-payments/code-server/pkg/code/data/badgecount" "github.com/code-payments/code-server/pkg/code/data/balance" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/data/commitment" "github.com/code-payments/code-server/pkg/code/data/contact" "github.com/code-payments/code-server/pkg/code/data/currency" @@ -59,7 +59,7 @@ import ( airdrop_memory_client "github.com/code-payments/code-server/pkg/code/data/airdrop/memory" badgecount_memory_client "github.com/code-payments/code-server/pkg/code/data/badgecount/memory" balance_memory_client "github.com/code-payments/code-server/pkg/code/data/balance/memory" - chat_memory_client "github.com/code-payments/code-server/pkg/code/data/chat/memory" + chat_v1_memory_client "github.com/code-payments/code-server/pkg/code/data/chat/v1/memory" commitment_memory_client "github.com/code-payments/code-server/pkg/code/data/commitment/memory" contact_memory_client "github.com/code-payments/code-server/pkg/code/data/contact/memory" currency_memory_client "github.com/code-payments/code-server/pkg/code/data/currency/memory" @@ -94,7 +94,7 @@ import ( airdrop_postgres_client "github.com/code-payments/code-server/pkg/code/data/airdrop/postgres" badgecount_postgres_client "github.com/code-payments/code-server/pkg/code/data/badgecount/postgres" balance_postgres_client "github.com/code-payments/code-server/pkg/code/data/balance/postgres" - chat_postgres_client "github.com/code-payments/code-server/pkg/code/data/chat/postgres" + chat_v1_postgres_client "github.com/code-payments/code-server/pkg/code/data/chat/v1/postgres" commitment_postgres_client "github.com/code-payments/code-server/pkg/code/data/commitment/postgres" contact_postgres_client "github.com/code-payments/code-server/pkg/code/data/contact/postgres" currency_postgres_client "github.com/code-payments/code-server/pkg/code/data/currency/postgres" @@ -378,19 +378,19 @@ type DatabaseData interface { CountWebhookByState(ctx context.Context, state webhook.State) (uint64, error) GetAllPendingWebhooksReadyToSend(ctx context.Context, limit uint64) ([]*webhook.Record, error) - // Chat + // Chat V1 // -------------------------------------------------------------------------------- - PutChat(ctx context.Context, record *chat.Chat) error - GetChatById(ctx context.Context, chatId chat.ChatId) (*chat.Chat, error) - GetAllChatsForUser(ctx context.Context, user string, opts ...query.Option) ([]*chat.Chat, error) - PutChatMessage(ctx context.Context, record *chat.Message) error - DeleteChatMessage(ctx context.Context, chatId chat.ChatId, messageId string) error - GetChatMessage(ctx context.Context, chatId chat.ChatId, messageId string) (*chat.Message, error) - GetAllChatMessages(ctx context.Context, chatId chat.ChatId, opts ...query.Option) ([]*chat.Message, error) - AdvanceChatPointer(ctx context.Context, chatId chat.ChatId, pointer string) error - GetChatUnreadCount(ctx context.Context, chatId chat.ChatId) (uint32, error) - SetChatMuteState(ctx context.Context, chatId chat.ChatId, isMuted bool) error - SetChatSubscriptionState(ctx context.Context, chatId chat.ChatId, isSubscribed bool) error + PutChatV1(ctx context.Context, record *chat_v1.Chat) error + GetChatByIdV1(ctx context.Context, chatId chat_v1.ChatId) (*chat_v1.Chat, error) + GetAllChatsForUserV1(ctx context.Context, user string, opts ...query.Option) ([]*chat_v1.Chat, error) + PutChatMessageV1(ctx context.Context, record *chat_v1.Message) error + DeleteChatMessageV1(ctx context.Context, chatId chat_v1.ChatId, messageId string) error + GetChatMessageV1(ctx context.Context, chatId chat_v1.ChatId, messageId string) (*chat_v1.Message, error) + GetAllChatMessagesV1(ctx context.Context, chatId chat_v1.ChatId, opts ...query.Option) ([]*chat_v1.Message, error) + AdvanceChatPointerV1(ctx context.Context, chatId chat_v1.ChatId, pointer string) error + GetChatUnreadCountV1(ctx context.Context, chatId chat_v1.ChatId) (uint32, error) + SetChatMuteStateV1(ctx context.Context, chatId chat_v1.ChatId, isMuted bool) error + SetChatSubscriptionStateV1(ctx context.Context, chatId chat_v1.ChatId, isSubscribed bool) error // Badge Count // -------------------------------------------------------------------------------- @@ -470,7 +470,7 @@ type DatabaseProvider struct { paywall paywall.Store event event.Store webhook webhook.Store - chat chat.Store + chatv1 chat_v1.Store badgecount badgecount.Store login login.Store balance balance.Store @@ -532,7 +532,7 @@ func NewDatabaseProvider(dbConfig *pg.Config) (DatabaseData, error) { paywall: paywall_postgres_client.New(db), event: event_postgres_client.New(db), webhook: webhook_postgres_client.New(db), - chat: chat_postgres_client.New(db), + chatv1: chat_v1_postgres_client.New(db), badgecount: badgecount_postgres_client.New(db), login: login_postgres_client.New(db), balance: balance_postgres_client.New(db), @@ -575,7 +575,7 @@ func NewTestDatabaseProvider() DatabaseData { paywall: paywall_memory_client.New(), event: event_memory_client.New(), webhook: webhook_memory_client.New(), - chat: chat_memory_client.New(), + chatv1: chat_v1_memory_client.New(), badgecount: badgecount_memory_client.New(), login: login_memory_client.New(), balance: balance_memory_client.New(), @@ -1399,48 +1399,48 @@ func (dp *DatabaseProvider) GetAllPendingWebhooksReadyToSend(ctx context.Context return dp.webhook.GetAllPendingReadyToSend(ctx, limit) } -// Chat +// Chat V1 // -------------------------------------------------------------------------------- -func (dp *DatabaseProvider) PutChat(ctx context.Context, record *chat.Chat) error { - return dp.chat.PutChat(ctx, record) +func (dp *DatabaseProvider) PutChatV1(ctx context.Context, record *chat_v1.Chat) error { + return dp.chatv1.PutChat(ctx, record) } -func (dp *DatabaseProvider) GetChatById(ctx context.Context, chatId chat.ChatId) (*chat.Chat, error) { - return dp.chat.GetChatById(ctx, chatId) +func (dp *DatabaseProvider) GetChatByIdV1(ctx context.Context, chatId chat_v1.ChatId) (*chat_v1.Chat, error) { + return dp.chatv1.GetChatById(ctx, chatId) } -func (dp *DatabaseProvider) GetAllChatsForUser(ctx context.Context, user string, opts ...query.Option) ([]*chat.Chat, error) { +func (dp *DatabaseProvider) GetAllChatsForUserV1(ctx context.Context, user string, opts ...query.Option) ([]*chat_v1.Chat, error) { req, err := query.DefaultPaginationHandler(opts...) if err != nil { return nil, err } - return dp.chat.GetAllChatsForUser(ctx, user, req.Cursor, req.SortBy, req.Limit) + return dp.chatv1.GetAllChatsForUser(ctx, user, req.Cursor, req.SortBy, req.Limit) } -func (dp *DatabaseProvider) PutChatMessage(ctx context.Context, record *chat.Message) error { - return dp.chat.PutMessage(ctx, record) +func (dp *DatabaseProvider) PutChatMessageV1(ctx context.Context, record *chat_v1.Message) error { + return dp.chatv1.PutMessage(ctx, record) } -func (dp *DatabaseProvider) DeleteChatMessage(ctx context.Context, chatId chat.ChatId, messageId string) error { - return dp.chat.DeleteMessage(ctx, chatId, messageId) +func (dp *DatabaseProvider) DeleteChatMessageV1(ctx context.Context, chatId chat_v1.ChatId, messageId string) error { + return dp.chatv1.DeleteMessage(ctx, chatId, messageId) } -func (dp *DatabaseProvider) GetChatMessage(ctx context.Context, chatId chat.ChatId, messageId string) (*chat.Message, error) { - return dp.chat.GetMessageById(ctx, chatId, messageId) +func (dp *DatabaseProvider) GetChatMessageV1(ctx context.Context, chatId chat_v1.ChatId, messageId string) (*chat_v1.Message, error) { + return dp.chatv1.GetMessageById(ctx, chatId, messageId) } -func (dp *DatabaseProvider) GetAllChatMessages(ctx context.Context, chatId chat.ChatId, opts ...query.Option) ([]*chat.Message, error) { +func (dp *DatabaseProvider) GetAllChatMessagesV1(ctx context.Context, chatId chat_v1.ChatId, opts ...query.Option) ([]*chat_v1.Message, error) { req, err := query.DefaultPaginationHandler(opts...) if err != nil { return nil, err } - return dp.chat.GetAllMessagesByChat(ctx, chatId, req.Cursor, req.SortBy, req.Limit) + return dp.chatv1.GetAllMessagesByChat(ctx, chatId, req.Cursor, req.SortBy, req.Limit) } -func (dp *DatabaseProvider) AdvanceChatPointer(ctx context.Context, chatId chat.ChatId, pointer string) error { - return dp.chat.AdvancePointer(ctx, chatId, pointer) +func (dp *DatabaseProvider) AdvanceChatPointerV1(ctx context.Context, chatId chat_v1.ChatId, pointer string) error { + return dp.chatv1.AdvancePointer(ctx, chatId, pointer) } -func (dp *DatabaseProvider) GetChatUnreadCount(ctx context.Context, chatId chat.ChatId) (uint32, error) { - return dp.chat.GetUnreadCount(ctx, chatId) +func (dp *DatabaseProvider) GetChatUnreadCountV1(ctx context.Context, chatId chat_v1.ChatId) (uint32, error) { + return dp.chatv1.GetUnreadCount(ctx, chatId) } -func (dp *DatabaseProvider) SetChatMuteState(ctx context.Context, chatId chat.ChatId, isMuted bool) error { - return dp.chat.SetMuteState(ctx, chatId, isMuted) +func (dp *DatabaseProvider) SetChatMuteStateV1(ctx context.Context, chatId chat_v1.ChatId, isMuted bool) error { + return dp.chatv1.SetMuteState(ctx, chatId, isMuted) } -func (dp *DatabaseProvider) SetChatSubscriptionState(ctx context.Context, chatId chat.ChatId, isSubscribed bool) error { - return dp.chat.SetSubscriptionState(ctx, chatId, isSubscribed) +func (dp *DatabaseProvider) SetChatSubscriptionStateV1(ctx context.Context, chatId chat_v1.ChatId, isSubscribed bool) error { + return dp.chatv1.SetSubscriptionState(ctx, chatId, isSubscribed) } // Badge Count diff --git a/pkg/code/push/notifications.go b/pkg/code/push/notifications.go index 2635dddd..bfe4db7c 100644 --- a/pkg/code/push/notifications.go +++ b/pkg/code/push/notifications.go @@ -14,7 +14,7 @@ import ( chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/localization" "github.com/code-payments/code-server/pkg/code/thirdparty" currency_lib "github.com/code-payments/code-server/pkg/currency" @@ -59,13 +59,13 @@ func SendDepositPushNotification( // Legacy push notification still considers chat mute state // // todo: Proper migration to chat system - chatRecord, err := data.GetChatById(ctx, chat.GetChatId(chat_util.CashTransactionsName, owner.PublicKey().ToBase58(), true)) + chatRecord, err := data.GetChatByIdV1(ctx, chat_v1.GetChatId(chat_util.CashTransactionsName, owner.PublicKey().ToBase58(), true)) switch err { case nil: if chatRecord.IsMuted { return nil } - case chat.ErrChatNotFound: + case chat_v1.ErrChatNotFound: default: log.WithError(err).Warn("failure getting chat record") return errors.Wrap(err, "error getting chat record") @@ -139,13 +139,13 @@ func SendGiftCardReturnedPushNotification( // Legacy push notification still considers chat mute state // // todo: Proper migration to chat system - chatRecord, err := data.GetChatById(ctx, chat.GetChatId(chat_util.CashTransactionsName, owner.PublicKey().ToBase58(), true)) + chatRecord, err := data.GetChatByIdV1(ctx, chat_v1.GetChatId(chat_util.CashTransactionsName, owner.PublicKey().ToBase58(), true)) switch err { case nil: if chatRecord.IsMuted { return nil } - case chat.ErrChatNotFound: + case chat_v1.ErrChatNotFound: default: log.WithError(err).Warn("failure getting chat record") return errors.Wrap(err, "error getting chat record") diff --git a/pkg/code/server/grpc/chat/server.go b/pkg/code/server/grpc/chat/v1/server.go similarity index 96% rename from pkg/code/server/grpc/chat/server.go rename to pkg/code/server/grpc/chat/v1/server.go index b2b9c58f..5fef09f9 100644 --- a/pkg/code/server/grpc/chat/server.go +++ b/pkg/code/server/grpc/chat/v1/server.go @@ -1,4 +1,4 @@ -package chat +package chat_v1 import ( "bytes" @@ -24,7 +24,7 @@ import ( chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/localization" push_util "github.com/code-payments/code-server/pkg/code/push" "github.com/code-payments/code-server/pkg/database/query" @@ -59,7 +59,7 @@ type server struct { func NewChatServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier, pusher push_lib.Provider) chatpb.ChatServer { s := &server{ - log: logrus.StandardLogger().WithField("type", "chat/server"), + log: logrus.StandardLogger().WithField("type", "chat/v1/server"), data: data, auth: auth, pusher: pusher, @@ -119,7 +119,7 @@ func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*ch } } - chatRecords, err := s.data.GetAllChatsForUser( + chatRecords, err := s.data.GetAllChatsForUserV1( ctx, owner.PublicKey().ToBase58(), query.WithCursor(cursor), @@ -220,7 +220,7 @@ func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*ch if !skipUnreadCountQuery && !chatRecord.IsMuted && !chatRecord.IsUnsubscribed { // todo: will need batching when users have a large number of chats - unreadCount, err := s.data.GetChatUnreadCount(ctx, chatRecord.ChatId) + unreadCount, err := s.data.GetChatUnreadCountV1(ctx, chatRecord.ChatId) if err != nil { log.WithError(err).Warn("failure getting unread count") } @@ -260,7 +260,7 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest return nil, err } - chatRecord, err := s.data.GetChatById(ctx, chatId) + chatRecord, err := s.data.GetChatByIdV1(ctx, chatId) if err == chat.ErrChatNotFound { return &chatpb.GetMessagesResponse{ Result: chatpb.GetMessagesResponse_NOT_FOUND, @@ -296,7 +296,7 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest cursor = req.Cursor.Value } - messageRecords, err := s.data.GetAllChatMessages( + messageRecords, err := s.data.GetAllChatMessagesV1( ctx, chatId, query.WithCursor(cursor), @@ -416,7 +416,7 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR return nil, status.Error(codes.InvalidArgument, "Pointer.Kind must be READ") } - chatRecord, err := s.data.GetChatById(ctx, chatId) + chatRecord, err := s.data.GetChatByIdV1(ctx, chatId) if err == chat.ErrChatNotFound { return &chatpb.AdvancePointerResponse{ Result: chatpb.AdvancePointerResponse_CHAT_NOT_FOUND, @@ -430,7 +430,7 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR return nil, status.Error(codes.PermissionDenied, "") } - newPointerRecord, err := s.data.GetChatMessage(ctx, chatId, messageId) + newPointerRecord, err := s.data.GetChatMessageV1(ctx, chatId, messageId) if err == chat.ErrMessageNotFound { return &chatpb.AdvancePointerResponse{ Result: chatpb.AdvancePointerResponse_MESSAGE_NOT_FOUND, @@ -441,7 +441,7 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR } if chatRecord.ReadPointer != nil { - oldPointerRecord, err := s.data.GetChatMessage(ctx, chatId, *chatRecord.ReadPointer) + oldPointerRecord, err := s.data.GetChatMessageV1(ctx, chatId, *chatRecord.ReadPointer) if err != nil { log.WithError(err).Warn("failure getting chat message record for old pointer value") return nil, status.Error(codes.Internal, "") @@ -454,7 +454,7 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR } } - err = s.data.AdvanceChatPointer(ctx, chatId, messageId) + err = s.data.AdvanceChatPointerV1(ctx, chatId, messageId) if err != nil { log.WithError(err).Warn("failure advancing pointer") return nil, status.Error(codes.Internal, "") @@ -484,7 +484,7 @@ func (s *server) SetMuteState(ctx context.Context, req *chatpb.SetMuteStateReque return nil, err } - chatRecord, err := s.data.GetChatById(ctx, chatId) + chatRecord, err := s.data.GetChatByIdV1(ctx, chatId) if err == chat.ErrChatNotFound { return &chatpb.SetMuteStateResponse{ Result: chatpb.SetMuteStateResponse_CHAT_NOT_FOUND, @@ -511,7 +511,7 @@ func (s *server) SetMuteState(ctx context.Context, req *chatpb.SetMuteStateReque }, nil } - err = s.data.SetChatMuteState(ctx, chatId, req.IsMuted) + err = s.data.SetChatMuteStateV1(ctx, chatId, req.IsMuted) if err != nil { log.WithError(err).Warn("failure setting mute status") return nil, status.Error(codes.Internal, "") @@ -542,7 +542,7 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr return nil, err } - chatRecord, err := s.data.GetChatById(ctx, chatId) + chatRecord, err := s.data.GetChatByIdV1(ctx, chatId) if err == chat.ErrChatNotFound { return &chatpb.SetSubscriptionStateResponse{ Result: chatpb.SetSubscriptionStateResponse_CHAT_NOT_FOUND, @@ -569,7 +569,7 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr }, nil } - err = s.data.SetChatSubscriptionState(ctx, chatId, req.IsSubscribed) + err = s.data.SetChatSubscriptionStateV1(ctx, chatId, req.IsSubscribed) if err != nil { log.WithError(err).Warn("failure setting subcription status") return nil, status.Error(codes.Internal, "") diff --git a/pkg/code/server/grpc/chat/server_test.go b/pkg/code/server/grpc/chat/v1/server_test.go similarity index 99% rename from pkg/code/server/grpc/chat/server_test.go rename to pkg/code/server/grpc/chat/v1/server_test.go index 54af7c84..0a9abad4 100644 --- a/pkg/code/server/grpc/chat/server_test.go +++ b/pkg/code/server/grpc/chat/v1/server_test.go @@ -1,4 +1,4 @@ -package chat +package chat_v1 import ( "context" @@ -22,7 +22,7 @@ import ( chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/data/phone" "github.com/code-payments/code-server/pkg/code/data/preferences" "github.com/code-payments/code-server/pkg/code/data/user" diff --git a/pkg/code/server/grpc/chat/stream.go b/pkg/code/server/grpc/chat/v1/stream.go similarity index 97% rename from pkg/code/server/grpc/chat/stream.go rename to pkg/code/server/grpc/chat/v1/stream.go index 3f6ca6fa..05b63235 100644 --- a/pkg/code/server/grpc/chat/stream.go +++ b/pkg/code/server/grpc/chat/v1/stream.go @@ -1,4 +1,4 @@ -package chat +package chat_v1 import ( "context" @@ -14,7 +14,7 @@ import ( chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1" "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v1" ) const ( diff --git a/pkg/code/server/grpc/transaction/v2/history_test.go b/pkg/code/server/grpc/transaction/v2/history_test.go index f2cddee5..80ae0442 100644 --- a/pkg/code/server/grpc/transaction/v2/history_test.go +++ b/pkg/code/server/grpc/transaction/v2/history_test.go @@ -12,7 +12,7 @@ import ( chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" currency_lib "github.com/code-payments/code-server/pkg/currency" "github.com/code-payments/code-server/pkg/kin" timelock_token_v1 "github.com/code-payments/code-server/pkg/solana/timelock/v1" @@ -142,7 +142,7 @@ func TestPaymentHistory_HappyPath(t *testing.T) { sendingPhone.tip456KinToCodeUser(t, receivingPhone, twitterUsername).requireSuccess(t) - chatMessageRecords, err := server.data.GetAllChatMessages(server.ctx, chat.GetChatId(chat_util.CashTransactionsName, sendingPhone.parentAccount.PublicKey().ToBase58(), true)) + chatMessageRecords, err := server.data.GetAllChatMessagesV1(server.ctx, chat_v1.GetChatId(chat_util.CashTransactionsName, sendingPhone.parentAccount.PublicKey().ToBase58(), true)) require.NoError(t, err) require.Len(t, chatMessageRecords, 10) @@ -236,7 +236,7 @@ func TestPaymentHistory_HappyPath(t *testing.T) { assert.Equal(t, 32.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) assert.Equal(t, kin.ToQuarks(321), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId("example.com", sendingPhone.parentAccount.PublicKey().ToBase58(), true)) + chatMessageRecords, err = server.data.GetAllChatMessagesV1(server.ctx, chat_v1.GetChatId("example.com", sendingPhone.parentAccount.PublicKey().ToBase58(), true)) require.NoError(t, err) require.Len(t, chatMessageRecords, 3) @@ -267,7 +267,7 @@ func TestPaymentHistory_HappyPath(t *testing.T) { assert.Equal(t, 123.0, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) assert.Equal(t, kin.ToQuarks(123), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId(chat_util.CashTransactionsName, receivingPhone.parentAccount.PublicKey().ToBase58(), true)) + chatMessageRecords, err = server.data.GetAllChatMessagesV1(server.ctx, chat_v1.GetChatId(chat_util.CashTransactionsName, receivingPhone.parentAccount.PublicKey().ToBase58(), true)) require.NoError(t, err) require.Len(t, chatMessageRecords, 7) @@ -334,7 +334,7 @@ func TestPaymentHistory_HappyPath(t *testing.T) { assert.Equal(t, 2.1, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) assert.Equal(t, kin.ToQuarks(42), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId(chat_util.TipsName, sendingPhone.parentAccount.PublicKey().ToBase58(), true)) + chatMessageRecords, err = server.data.GetAllChatMessagesV1(server.ctx, chat_v1.GetChatId(chat_util.TipsName, sendingPhone.parentAccount.PublicKey().ToBase58(), true)) require.NoError(t, err) require.Len(t, chatMessageRecords, 1) @@ -347,7 +347,7 @@ func TestPaymentHistory_HappyPath(t *testing.T) { assert.Equal(t, 45.6, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) assert.Equal(t, kin.ToQuarks(456), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId("example.com", receivingPhone.parentAccount.PublicKey().ToBase58(), true)) + chatMessageRecords, err = server.data.GetAllChatMessagesV1(server.ctx, chat_v1.GetChatId("example.com", receivingPhone.parentAccount.PublicKey().ToBase58(), true)) require.NoError(t, err) require.Len(t, chatMessageRecords, 5) @@ -396,7 +396,7 @@ func TestPaymentHistory_HappyPath(t *testing.T) { assert.Equal(t, 12_345.0, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) assert.Equal(t, kin.ToQuarks(12_345), protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId("example.com", receivingPhone.parentAccount.PublicKey().ToBase58(), false)) + chatMessageRecords, err = server.data.GetAllChatMessagesV1(server.ctx, chat_v1.GetChatId("example.com", receivingPhone.parentAccount.PublicKey().ToBase58(), false)) require.NoError(t, err) require.Len(t, chatMessageRecords, 1) @@ -409,7 +409,7 @@ func TestPaymentHistory_HappyPath(t *testing.T) { assert.Equal(t, 77.69, protoChatMessage.Content[0].GetExchangeData().GetExact().NativeAmount) assert.EqualValues(t, 77690000, protoChatMessage.Content[0].GetExchangeData().GetExact().Quarks) - chatMessageRecords, err = server.data.GetAllChatMessages(server.ctx, chat.GetChatId(chat_util.TipsName, receivingPhone.parentAccount.PublicKey().ToBase58(), true)) + chatMessageRecords, err = server.data.GetAllChatMessagesV1(server.ctx, chat_v1.GetChatId(chat_util.TipsName, receivingPhone.parentAccount.PublicKey().ToBase58(), true)) require.NoError(t, err) require.Len(t, chatMessageRecords, 1) diff --git a/pkg/code/server/grpc/transaction/v2/swap.go b/pkg/code/server/grpc/transaction/v2/swap.go index 792e3982..d38a18fd 100644 --- a/pkg/code/server/grpc/transaction/v2/swap.go +++ b/pkg/code/server/grpc/transaction/v2/swap.go @@ -22,7 +22,7 @@ import ( chat_util "github.com/code-payments/code-server/pkg/code/chat" "github.com/code-payments/code-server/pkg/code/common" "github.com/code-payments/code-server/pkg/code/data/account" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/localization" push_util "github.com/code-payments/code-server/pkg/code/push" currency_lib "github.com/code-payments/code-server/pkg/currency" @@ -510,7 +510,7 @@ func (s *transactionServer) bestEffortNotifyUserOfSwapInProgress(ctx context.Con // Inspect the chat history for a USDC deposited message. If that message // doesn't exist, then avoid sending the swap in progress chat message, since // it can lead to user confusion. - chatMessageRecords, err := s.data.GetAllChatMessages(ctx, chatId, query.WithDirection(query.Descending), query.WithLimit(1)) + chatMessageRecords, err := s.data.GetAllChatMessagesV1(ctx, chatId, query.WithDirection(query.Descending), query.WithLimit(1)) switch err { case nil: var protoChatMessage chatpb.ChatMessage @@ -525,7 +525,7 @@ func (s *transactionServer) bestEffortNotifyUserOfSwapInProgress(ctx context.Con return nil } } - case chat.ErrMessageNotFound: + case chat_v1.ErrMessageNotFound: default: return errors.Wrap(err, "error fetching chat messages") } diff --git a/pkg/code/server/grpc/transaction/v2/testutil.go b/pkg/code/server/grpc/transaction/v2/testutil.go index 38c42626..1da54d65 100644 --- a/pkg/code/server/grpc/transaction/v2/testutil.go +++ b/pkg/code/server/grpc/transaction/v2/testutil.go @@ -35,7 +35,7 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" "github.com/code-payments/code-server/pkg/code/data/account" "github.com/code-payments/code-server/pkg/code/data/action" - "github.com/code-payments/code-server/pkg/code/data/chat" + chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" "github.com/code-payments/code-server/pkg/code/data/commitment" "github.com/code-payments/code-server/pkg/code/data/currency" "github.com/code-payments/code-server/pkg/code/data/deposit" @@ -6173,7 +6173,7 @@ func isSubmitIntentError(resp *transactionpb.SubmitIntentResponse, err error) bo return err != nil || resp.GetError() != nil } -func getProtoChatMessage(t *testing.T, record *chat.Message) *chatpb.ChatMessage { +func getProtoChatMessage(t *testing.T, record *chat_v1.Message) *chatpb.ChatMessage { var protoMessage chatpb.ChatMessage require.NoError(t, proto.Unmarshal(record.Data, &protoMessage)) return &protoMessage From a8103da4de6e477662af9e21b454d01e41e5b841 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 7 Jun 2024 10:55:49 -0400 Subject: [PATCH 11/40] Add skeleton for v2 gRPC chat service --- pkg/code/server/grpc/chat/v2/server.go | 190 ++++++++++++++++++++ pkg/code/server/grpc/chat/v2/server_test.go | 1 + pkg/code/server/grpc/chat/v2/stream.go | 32 ++++ 3 files changed, 223 insertions(+) create mode 100644 pkg/code/server/grpc/chat/v2/server.go create mode 100644 pkg/code/server/grpc/chat/v2/server_test.go create mode 100644 pkg/code/server/grpc/chat/v2/stream.go diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go new file mode 100644 index 00000000..37319f40 --- /dev/null +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -0,0 +1,190 @@ +package chat_v2 + +import ( + "context" + "time" + + "github.com/sirupsen/logrus" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v2" + + auth_util "github.com/code-payments/code-server/pkg/code/auth" + "github.com/code-payments/code-server/pkg/code/common" + code_data "github.com/code-payments/code-server/pkg/code/data" + "github.com/code-payments/code-server/pkg/grpc/client" +) + +// todo: Ensure all relevant logging fields are set +type server struct { + log *logrus.Entry + + data code_data.Provider + auth *auth_util.RPCSignatureVerifier + + chatpb.UnimplementedChatServer +} + +func NewChatServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier) chatpb.ChatServer { + return &server{ + log: logrus.StandardLogger().WithField("type", "chat/v2/server"), + data: data, + auth: auth, + } +} + +func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*chatpb.GetChatsResponse, error) { + log := s.log.WithField("method", "GetChats") + log = client.InjectLoggingMetadata(ctx, log) + + owner, err := common.NewAccountFromProto(req.Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + + signature := req.Signature + req.Signature = nil + if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { + return nil, err + } + + return nil, status.Error(codes.Unimplemented, "") +} + +func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest) (*chatpb.GetMessagesResponse, error) { + log := s.log.WithField("method", "GetMessages") + log = client.InjectLoggingMetadata(ctx, log) + + owner, err := common.NewAccountFromProto(req.Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + + signature := req.Signature + req.Signature = nil + if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { + return nil, err + } + + return nil, status.Error(codes.Unimplemented, "") +} + +func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) error { + ctx := streamer.Context() + + log := s.log.WithField("method", "StreamChatEvents") + log = client.InjectLoggingMetadata(ctx, log) + + req, err := boundedStreamChatEventsRecv(ctx, streamer, 250*time.Millisecond) + if err != nil { + return err + } + + if req.GetOpenStream() == nil { + return status.Error(codes.InvalidArgument, "open_stream is nil") + } + + if req.GetOpenStream().Signature == nil { + return status.Error(codes.InvalidArgument, "signature is nil") + } + + owner, err := common.NewAccountFromProto(req.GetOpenStream().Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return status.Error(codes.Internal, "") + } + log = log.WithField("owner", owner.PublicKey().ToBase58()) + + signature := req.GetOpenStream().Signature + req.GetOpenStream().Signature = nil + if err = s.auth.Authenticate(streamer.Context(), owner, req.GetOpenStream(), signature); err != nil { + return err + } + + return status.Error(codes.Unimplemented, "") +} + +func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest) (*chatpb.SendMessageResponse, error) { + log := s.log.WithField("method", "SendMessage") + log = client.InjectLoggingMetadata(ctx, log) + + owner, err := common.NewAccountFromProto(req.Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("owner", owner.PublicKey().ToBase58()) + + signature := req.Signature + req.Signature = nil + if err = s.auth.Authenticate(ctx, owner, req, signature); err != nil { + return nil, err + } + + return nil, status.Error(codes.Unimplemented, "") +} + +func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerRequest) (*chatpb.AdvancePointerResponse, error) { + log := s.log.WithField("method", "AdvancePointer") + log = client.InjectLoggingMetadata(ctx, log) + + owner, err := common.NewAccountFromProto(req.Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + + signature := req.Signature + req.Signature = nil + if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { + return nil, err + } + + return nil, status.Error(codes.Unimplemented, "") +} + +func (s *server) SetMuteState(ctx context.Context, req *chatpb.SetMuteStateRequest) (*chatpb.SetMuteStateResponse, error) { + log := s.log.WithField("method", "SetMuteState") + log = client.InjectLoggingMetadata(ctx, log) + + owner, err := common.NewAccountFromProto(req.Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + + signature := req.Signature + req.Signature = nil + if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { + return nil, err + } + + return nil, status.Error(codes.Unimplemented, "") +} + +func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscriptionStateRequest) (*chatpb.SetSubscriptionStateResponse, error) { + log := s.log.WithField("method", "SetSubscriptionState") + log = client.InjectLoggingMetadata(ctx, log) + + owner, err := common.NewAccountFromProto(req.Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + + signature := req.Signature + req.Signature = nil + if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { + return nil, err + } + + return nil, status.Error(codes.Unimplemented, "") +} diff --git a/pkg/code/server/grpc/chat/v2/server_test.go b/pkg/code/server/grpc/chat/v2/server_test.go new file mode 100644 index 00000000..aacc4f95 --- /dev/null +++ b/pkg/code/server/grpc/chat/v2/server_test.go @@ -0,0 +1 @@ +package chat_v2 diff --git a/pkg/code/server/grpc/chat/v2/stream.go b/pkg/code/server/grpc/chat/v2/stream.go new file mode 100644 index 00000000..bf986572 --- /dev/null +++ b/pkg/code/server/grpc/chat/v2/stream.go @@ -0,0 +1,32 @@ +package chat_v2 + +import ( + "context" + "time" + + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v2" +) + +func boundedStreamChatEventsRecv( + ctx context.Context, + streamer chatpb.Chat_StreamChatEventsServer, + timeout time.Duration, +) (req *chatpb.StreamChatEventsRequest, err error) { + done := make(chan struct{}) + go func() { + req, err = streamer.Recv() + close(done) + }() + + select { + case <-done: + return req, err + case <-ctx.Done(): + return nil, status.Error(codes.Canceled, "") + case <-time.After(timeout): + return nil, status.Error(codes.DeadlineExceeded, "timed out receiving message") + } +} From 3b0c6af619f1951864b7f896c3ff13637a9e3ffb Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 7 Jun 2024 12:16:22 -0400 Subject: [PATCH 12/40] Define chat v2 models --- pkg/code/data/chat/v2/id.go | 182 +++++++++++++++ pkg/code/data/chat/v2/id_test.go | 55 +++++ pkg/code/data/chat/v2/model.go | 370 +++++++++++++++++++++++++++++++ pkg/code/data/chat/v2/store.go | 9 + 4 files changed, 616 insertions(+) create mode 100644 pkg/code/data/chat/v2/id.go create mode 100644 pkg/code/data/chat/v2/id_test.go create mode 100644 pkg/code/data/chat/v2/model.go create mode 100644 pkg/code/data/chat/v2/store.go diff --git a/pkg/code/data/chat/v2/id.go b/pkg/code/data/chat/v2/id.go new file mode 100644 index 00000000..267ae7f2 --- /dev/null +++ b/pkg/code/data/chat/v2/id.go @@ -0,0 +1,182 @@ +package chat_v2 + +import ( + "bytes" + "encoding/hex" + "time" + + "github.com/google/uuid" + "github.com/pkg/errors" + + chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v2" +) + +type ChatId [32]byte + +// GetChatIdFromProto gets a chat ID from the protobuf variant +func GetChatIdFromProto(proto *chatpb.ChatId) (ChatId, error) { + if err := proto.Validate(); err != nil { + return ChatId{}, errors.Wrap(err, "proto validation failed") + } + + var typed ChatId + copy(typed[:], proto.Value) + + if err := typed.Validate(); err != nil { + return ChatId{}, errors.Wrap(err, "invalid chat id") + } + + return typed, nil +} + +// Validate validates a chat ID +func (c ChatId) Validate() error { + return nil +} + +// String returns the string representation of a ChatId +func (c ChatId) String() string { + return hex.EncodeToString(c[:]) +} + +// Random UUIDv4 ID for chat members +type MemberId uuid.UUID + +// GetMemberIdFromProto gets a member ID from the protobuf variant +func GetMemberIdFromProto(proto *chatpb.ChatMemberId) (MemberId, error) { + if err := proto.Validate(); err != nil { + return MemberId{}, errors.Wrap(err, "proto validation failed") + } + + var typed MemberId + copy(typed[:], proto.Value) + + if err := typed.Validate(); err != nil { + return MemberId{}, errors.Wrap(err, "invalid member id") + } + + return typed, nil +} + +// GenerateMemberId generates a new random chat member ID +func GenerateMemberId() MemberId { + return MemberId(uuid.New()) +} + +// Validate validates a chat member ID +func (m MemberId) Validate() error { + casted := uuid.UUID(m) + + if casted.Version() != 4 { + return errors.Errorf("invalid uuid version: %s", casted.Version().String()) + } + + return nil +} + +// String returns the string representation of a MemberId +func (m MemberId) String() string { + return uuid.UUID(m).String() +} + +// Time-based UUIDv7 ID for chat messages +type MessageId uuid.UUID + +// GenerateMessageId generates a UUIDv7 message ID using the current time +func GenerateMessageId() MessageId { + return GenerateMessageIdAtTime(time.Now()) +} + +// GenerateMessageIdAtTime generates a UUIDv7 message ID using the provided timestamp +func GenerateMessageIdAtTime(ts time.Time) MessageId { + // Convert timestamp to milliseconds since Unix epoch + millis := ts.UnixNano() / int64(time.Millisecond) + + // Create a byte slice to hold the UUID + var uuidBytes [16]byte + + // Populate the first 6 bytes with the timestamp (42 bits for timestamp) + uuidBytes[0] = byte((millis >> 40) & 0xff) + uuidBytes[1] = byte((millis >> 32) & 0xff) + uuidBytes[2] = byte((millis >> 24) & 0xff) + uuidBytes[3] = byte((millis >> 16) & 0xff) + uuidBytes[4] = byte((millis >> 8) & 0xff) + uuidBytes[5] = byte(millis & 0xff) + + // Set the version to 7 (UUIDv7) + uuidBytes[6] = (uuidBytes[6] & 0x0f) | (0x7 << 4) + + // Populate the remaining bytes with random values + randomUUID := uuid.New() + copy(uuidBytes[7:], randomUUID[7:]) + + return MessageId(uuidBytes) +} + +// GetMessageIdFromProto gets a message ID from the protobuf variant +func GetMessageIdFromProto(proto *chatpb.ChatMessageId) (MessageId, error) { + if err := proto.Validate(); err != nil { + return MessageId{}, errors.Wrap(err, "proto validation failed") + } + + var typed MessageId + copy(typed[:], proto.Value) + + if err := typed.Validate(); err != nil { + return MessageId{}, errors.Wrap(err, "invalid message id") + } + + return typed, nil +} + +// GetTimestamp gets the encoded timestamp in the message ID +func (m MessageId) GetTimestamp() (time.Time, error) { + if err := m.Validate(); err != nil { + return time.Time{}, errors.Wrap(err, "invalid message id") + } + + // Extract the first 6 bytes as the timestamp + millis := (int64(m[0]) << 40) | (int64(m[1]) << 32) | (int64(m[2]) << 24) | + (int64(m[3]) << 16) | (int64(m[4]) << 8) | int64(m[5]) + + // Convert milliseconds since Unix epoch to time.Time + timestamp := time.Unix(0, millis*int64(time.Millisecond)) + + return timestamp, nil +} + +// Equals returns whether two message IDs are equal +func (m MessageId) Equals(other MessageId) bool { + return m.Compare(other) == 0 +} + +// Before returns whether the message ID is before the provided value +func (m MessageId) Before(other MessageId) bool { + return m.Compare(other) < 0 +} + +// Before returns whether the message ID is after the provided value +func (m MessageId) After(other MessageId) bool { + return m.Compare(other) > 0 +} + +// Compare returns the byte comparison of the message ID +func (m MessageId) Compare(other MessageId) int { + return bytes.Compare(m[:], other[:]) +} + +// Validate validates a message ID +func (m MessageId) Validate() error { + casted := uuid.UUID(m) + + if casted.Version() != 7 { + return errors.Errorf("invalid uuid version: %s", casted.Version().String()) + } + + return nil +} + +// String returns the string representation of a MessageId +func (m MessageId) String() string { + return uuid.UUID(m).String() +} diff --git a/pkg/code/data/chat/v2/id_test.go b/pkg/code/data/chat/v2/id_test.go new file mode 100644 index 00000000..27f1f359 --- /dev/null +++ b/pkg/code/data/chat/v2/id_test.go @@ -0,0 +1,55 @@ +package chat_v2 + +import ( + "testing" + "time" + + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGenerateMemberId_Validation(t *testing.T) { + valid := GenerateMemberId() + assert.NoError(t, valid.Validate()) + + invalid := MemberId(GenerateMessageId()) + assert.Error(t, invalid.Validate()) +} + +func TestGenerateMessageId_Validation(t *testing.T) { + valid := GenerateMessageId() + assert.NoError(t, valid.Validate()) + + invalid := MessageId(uuid.New()) + assert.Error(t, invalid.Validate()) +} + +func TestGenerateMessageId_TimestampExtraction(t *testing.T) { + expectedTs := time.Now() + + messageId := GenerateMessageIdAtTime(expectedTs) + actualTs, err := messageId.GetTimestamp() + require.NoError(t, err) + assert.Equal(t, expectedTs.UnixMilli(), actualTs.UnixMilli()) +} + +func TestGenerateMessageId_Ordering(t *testing.T) { + now := time.Now() + messageIds := make([]MessageId, 0) + for i := 0; i < 10; i++ { + messageId := GenerateMessageIdAtTime(now.Add(time.Duration(i * int(time.Millisecond)))) + messageIds = append(messageIds, messageId) + } + + for i := 0; i < len(messageIds)-1; i++ { + assert.True(t, messageIds[i].Equals(messageIds[i])) + assert.False(t, messageIds[i].Equals(messageIds[i+1])) + + assert.True(t, messageIds[i].Before(messageIds[i+1])) + assert.False(t, messageIds[i].After(messageIds[i+1])) + + assert.False(t, messageIds[i+1].Before(messageIds[i])) + assert.True(t, messageIds[i+1].After(messageIds[i])) + } +} diff --git a/pkg/code/data/chat/v2/model.go b/pkg/code/data/chat/v2/model.go new file mode 100644 index 00000000..d68a76eb --- /dev/null +++ b/pkg/code/data/chat/v2/model.go @@ -0,0 +1,370 @@ +package chat_v2 + +import ( + "time" + + "github.com/mr-tron/base58" + "github.com/pkg/errors" + + "github.com/code-payments/code-server/pkg/pointer" + + chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v2" +) + +type ChatType int + +const ( + ChatTypeUnknown ChatType = iota + ChatTypeNotification + ChatTypeTwoWay + // ChatTypeGroup +) + +type ReferenceType int + +const ( + ReferenceTypeUnknown ReferenceType = iota + ReferenceTypeIntent + ReferenceTypeSignature +) + +type PointerType int + +const ( + PointerTypeUnknown PointerType = iota + PointerTypeSent + PointerTypeDelivered + PointerTypeRead +) + +type Platform int + +const ( + PlatformUnknown Platform = iota + PlatformCode + PlatformTwitter +) + +type ChatRecord struct { + Id int64 + ChatId ChatId + + ChatType ChatType + + // Presence determined by ChatType: + // * Notification: Present, and may be a localization key + // * Two Way: Not present and generated dynamically based on chat members + // * Group: Present, and will not be a localization key + ChatTitle *string + + IsVerified bool + + CreatedAt time.Time +} + +type MemberRecord struct { + Id int64 + ChatId ChatId + MemberId MemberId + + Platform Platform + PlatformId string + + DeliveryPointer *MessageId + ReadPointer *MessageId + + IsMuted bool + IsUnsubscribed bool + + JoinedAt time.Time +} + +type MessageRecord struct { + Id int64 + ChatId ChatId + MessageId MessageId + + // Not present for notification-style chats + Sender *MemberId + + Data []byte + + ReferenceType *ReferenceType + Reference *string + + IsSilent bool + + // Note: No timestamp field, since it's encoded in MessageId +} + +// GetChatIdFromProto gets a chat ID from the protobuf variant +func GetPointerTypeFromProto(proto chatpb.Pointer_Kind) PointerType { + switch proto { + case chatpb.Pointer_SENT: + return PointerTypeSent + case chatpb.Pointer_DELIVERED: + return PointerTypeDelivered + case chatpb.Pointer_READ: + return PointerTypeRead + default: + return PointerTypeUnknown + } +} + +// String returns the string representation of the pointer type +func (p PointerType) String() string { + switch p { + case PointerTypeSent: + return "sent" + case PointerTypeDelivered: + return "delivered" + case PointerTypeRead: + return "read" + default: + return "unknown" + } +} + +// Validate validates a chat Record +func (r *ChatRecord) Validate() error { + if err := r.ChatId.Validate(); err != nil { + return errors.Wrap(err, "invalid chat id") + } + + switch r.ChatType { + case ChatTypeNotification: + if r.ChatTitle == nil || len(*r.ChatTitle) == 0 { + return errors.New("chat title is required for notification chats") + } + case ChatTypeTwoWay: + if r.ChatTitle != nil { + return errors.New("chat title cannot be set for two way chats") + } + default: + return errors.Errorf("invalid chat type: %d", r.ChatType) + } + + if r.CreatedAt.IsZero() { + return errors.New("creation timestamp is required") + } + + return nil +} + +// Clone clones a chat record +func (r *ChatRecord) Clone() ChatRecord { + return ChatRecord{ + Id: r.Id, + ChatId: r.ChatId, + + ChatType: r.ChatType, + + ChatTitle: pointer.StringCopy(r.ChatTitle), + + IsVerified: r.IsVerified, + + CreatedAt: r.CreatedAt, + } +} + +// CopyTo copies a chat record to the provided destination +func (r *ChatRecord) CopyTo(dst *ChatRecord) { + dst.Id = r.Id + dst.ChatId = r.ChatId + + dst.ChatType = r.ChatType + + dst.ChatTitle = pointer.StringCopy(r.ChatTitle) + + dst.IsVerified = r.IsVerified + + dst.CreatedAt = r.CreatedAt +} + +// Validate validates a member Record +func (r *MemberRecord) Validate() error { + if err := r.ChatId.Validate(); err != nil { + return errors.Wrap(err, "invalid chat id") + } + + if err := r.MemberId.Validate(); err != nil { + return errors.Wrap(err, "invalid member id") + } + + if len(r.PlatformId) == 0 { + return errors.New("platform id is required") + } + + switch r.Platform { + case PlatformCode: + decoded, err := base58.Decode(r.PlatformId) + if err != nil { + return errors.Wrap(err, "invalid base58 plaftorm id") + } + + if len(decoded) != 32 { + return errors.Wrap(err, "platform id is not a 32 byte buffer") + } + case PlatformTwitter: + if len(r.PlatformId) > 15 { + return errors.New("platform id must have at most 15 characters") + } + default: + return errors.Errorf("invalid plaftorm: %d", r.Platform) + } + + if r.DeliveryPointer != nil { + if err := r.DeliveryPointer.Validate(); err != nil { + return errors.Wrap(err, "invalid delivery pointer") + } + } + + if r.ReadPointer != nil { + if err := r.ReadPointer.Validate(); err != nil { + return errors.Wrap(err, "invalid read pointer") + } + } + + if r.JoinedAt.IsZero() { + return errors.New("joined timestamp is required") + } + + return nil +} + +// Clone clones a member record +func (r *MemberRecord) Clone() MemberRecord { + return MemberRecord{ + Id: r.Id, + ChatId: r.ChatId, + MemberId: r.MemberId, + + Platform: r.Platform, + PlatformId: r.PlatformId, + + DeliveryPointer: r.DeliveryPointer, // todo: pointer copy safety + ReadPointer: r.ReadPointer, // todo: pointer copy safety + + IsMuted: r.IsMuted, + IsUnsubscribed: r.IsUnsubscribed, + + JoinedAt: r.JoinedAt, + } +} + +// CopyTo copies a member record to the provided destination +func (r *MemberRecord) CopyTo(dst *MemberRecord) { + dst.Id = r.Id + dst.ChatId = r.ChatId + dst.MemberId = r.MemberId + + dst.Platform = r.Platform + dst.PlatformId = r.PlatformId + + dst.DeliveryPointer = r.DeliveryPointer // todo: pointer copy safety + dst.ReadPointer = r.ReadPointer // todo: pointer copy safety + + dst.IsMuted = r.IsMuted + dst.IsUnsubscribed = r.IsUnsubscribed + + dst.JoinedAt = r.JoinedAt +} + +// Validate validates a message Record +func (r *MessageRecord) Validate() error { + if err := r.ChatId.Validate(); err != nil { + return errors.Wrap(err, "invalid chat id") + } + + if err := r.MessageId.Validate(); err != nil { + return errors.Wrap(err, "invalid message id") + } + + if r.Sender != nil { + if err := r.Sender.Validate(); err != nil { + return errors.Wrap(err, "invalid sender id") + } + } + + if len(r.Data) == 0 { + return errors.New("message data is required") + } + + if r.Reference == nil && r.ReferenceType != nil { + return errors.New("reference is required when reference type is provided") + } + + if r.Reference != nil && r.ReferenceType == nil { + return errors.New("reference cannot be set when reference type is missing") + } + + if r.ReferenceType != nil { + switch *r.ReferenceType { + case ReferenceTypeIntent: + decoded, err := base58.Decode(*r.Reference) + if err != nil { + return errors.Wrap(err, "invalid base58 intent id reference") + } + + if len(decoded) != 32 { + return errors.Wrap(err, "reference is not a 32 byte buffer") + } + case ReferenceTypeSignature: + decoded, err := base58.Decode(*r.Reference) + if err != nil { + return errors.Wrap(err, "invalid base58 signature reference") + } + + if len(decoded) != 64 { + return errors.Wrap(err, "reference is not a 64 byte buffer") + } + default: + return errors.Errorf("invalid reference type: %d", *r.ReferenceType) + } + } + + return nil +} + +// Clone clones a message record +func (r *MessageRecord) Clone() MessageRecord { + return MessageRecord{ + Id: r.Id, + ChatId: r.ChatId, + MessageId: r.MessageId, + + Sender: r.Sender, // todo: pointer copy safety + + Data: r.Data, // todo: pointer copy safety + + ReferenceType: r.ReferenceType, // todo: pointer copy safety + Reference: r.Reference, // todo: pointer copy safety + + IsSilent: r.IsSilent, + + // todo: finish implementing me + } +} + +// CopyTo copies a message record to the provided destination +func (r *MessageRecord) CopyTo(dst *MessageRecord) { + dst.Id = r.Id + dst.ChatId = r.ChatId + dst.MessageId = r.MessageId + + dst.Sender = r.Sender // todo: pointer copy safety + + dst.Data = r.Data // todo: pointer copy safety + + dst.ReferenceType = r.ReferenceType // todo: pointer copy safety + dst.Reference = r.Reference // todo: pointer copy safety + + dst.IsSilent = r.IsSilent + + // todo: finish implementing me +} + +// GetTimestamp gets the timestamp for a message record +func (r *MessageRecord) GetTimestamp() (time.Time, error) { + return r.MessageId.GetTimestamp() +} diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go new file mode 100644 index 00000000..a4e9f858 --- /dev/null +++ b/pkg/code/data/chat/v2/store.go @@ -0,0 +1,9 @@ +package chat_v2 + +var ( +// todo: Define well-known errors here +) + +// todo: Define interface methods +type Store interface { +} From 66dfce4c7d72ec571ec9b6886b78ec8548f07285 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 7 Jun 2024 13:36:25 -0400 Subject: [PATCH 13/40] Implement RPCs that operate on chat member state --- pkg/code/data/chat/v2/store.go | 26 +++- pkg/code/data/internal.go | 34 +++++ pkg/code/server/grpc/chat/v2/server.go | 202 ++++++++++++++++++++++++- 3 files changed, 258 insertions(+), 4 deletions(-) diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index a4e9f858..b1d8d089 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -1,9 +1,33 @@ package chat_v2 +import ( + "context" + "errors" +) + var ( -// todo: Define well-known errors here + ErrChatNotFound = errors.New("chat not found") + ErrMemberNotFound = errors.New("chat member not found") + ErrMessageNotFound = errors.New("chat message not found") ) // todo: Define interface methods type Store interface { + // GetChatById gets a chat by its chat ID + GetChatById(ctx context.Context, chatId ChatId) (*ChatRecord, error) + + // GetMemberById gets a chat member by the chat and member IDs + GetMemberById(ctx context.Context, chatId ChatId, memberId MemberId) (*MemberRecord, error) + + // GetMessageById gets a chat message by the chat and message IDs + GetMessageById(ctx context.Context, chatId ChatId, messageId MessageId) (*MessageRecord, error) + + // AdvancePointer advances a chat pointer for a chat member + AdvancePointer(ctx context.Context, chatId ChatId, memberId MemberId, pointerType PointerType, pointer MessageId) error + + // SetMuteState updates the mute state for a chat member + SetMuteState(ctx context.Context, chatId ChatId, memberId MemberId, isMuted bool) error + + // SetSubscriptionState updates the subscription state for a chat member + SetSubscriptionState(ctx context.Context, chatId ChatId, memberId MemberId, isSubscribed bool) error } diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 95ce26f2..20a72601 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -26,6 +26,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/badgecount" "github.com/code-payments/code-server/pkg/code/data/balance" chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1" + chat_v2 "github.com/code-payments/code-server/pkg/code/data/chat/v2" "github.com/code-payments/code-server/pkg/code/data/commitment" "github.com/code-payments/code-server/pkg/code/data/contact" "github.com/code-payments/code-server/pkg/code/data/currency" @@ -392,6 +393,15 @@ type DatabaseData interface { SetChatMuteStateV1(ctx context.Context, chatId chat_v1.ChatId, isMuted bool) error SetChatSubscriptionStateV1(ctx context.Context, chatId chat_v1.ChatId, isSubscribed bool) error + // Chat V2 + // -------------------------------------------------------------------------------- + GetChatByIdV2(ctx context.Context, chatId chat_v2.ChatId) (*chat_v2.ChatRecord, error) + GetChatMemberByIdV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId) (*chat_v2.MemberRecord, error) + GetChatMessageByIdV2(ctx context.Context, chatId chat_v2.ChatId, messageId chat_v2.MessageId) (*chat_v2.MessageRecord, error) + AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) error + SetChatMuteStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isMuted bool) error + SetChatSubscriptionStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isSubscribed bool) error + // Badge Count // -------------------------------------------------------------------------------- AddToBadgeCount(ctx context.Context, owner string, amount uint32) error @@ -471,6 +481,7 @@ type DatabaseProvider struct { event event.Store webhook webhook.Store chatv1 chat_v1.Store + chatv2 chat_v2.Store badgecount badgecount.Store login login.Store balance balance.Store @@ -533,6 +544,7 @@ func NewDatabaseProvider(dbConfig *pg.Config) (DatabaseData, error) { event: event_postgres_client.New(db), webhook: webhook_postgres_client.New(db), chatv1: chat_v1_postgres_client.New(db), + chatv2: nil, // todo: Initialize me badgecount: badgecount_postgres_client.New(db), login: login_postgres_client.New(db), balance: balance_postgres_client.New(db), @@ -576,6 +588,7 @@ func NewTestDatabaseProvider() DatabaseData { event: event_memory_client.New(), webhook: webhook_memory_client.New(), chatv1: chat_v1_memory_client.New(), + chatv2: nil, // todo: initialize me badgecount: badgecount_memory_client.New(), login: login_memory_client.New(), balance: balance_memory_client.New(), @@ -1443,6 +1456,27 @@ func (dp *DatabaseProvider) SetChatSubscriptionStateV1(ctx context.Context, chat return dp.chatv1.SetSubscriptionState(ctx, chatId, isSubscribed) } +// Chat V2 +// -------------------------------------------------------------------------------- +func (dp *DatabaseProvider) GetChatByIdV2(ctx context.Context, chatId chat_v2.ChatId) (*chat_v2.ChatRecord, error) { + return dp.chatv2.GetChatById(ctx, chatId) +} +func (dp *DatabaseProvider) GetChatMemberByIdV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId) (*chat_v2.MemberRecord, error) { + return dp.chatv2.GetMemberById(ctx, chatId, memberId) +} +func (dp *DatabaseProvider) GetChatMessageByIdV2(ctx context.Context, chatId chat_v2.ChatId, messageId chat_v2.MessageId) (*chat_v2.MessageRecord, error) { + return dp.chatv2.GetMessageById(ctx, chatId, messageId) +} +func (dp *DatabaseProvider) AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) error { + return dp.chatv2.AdvancePointer(ctx, chatId, memberId, pointerType, pointer) +} +func (dp *DatabaseProvider) SetChatMuteStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isMuted bool) error { + return dp.chatv2.SetMuteState(ctx, chatId, memberId, isMuted) +} +func (dp *DatabaseProvider) SetChatSubscriptionStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isSubscribed bool) error { + return dp.chatv2.SetSubscriptionState(ctx, chatId, memberId, isSubscribed) +} + // Badge Count // -------------------------------------------------------------------------------- func (dp *DatabaseProvider) AddToBadgeCount(ctx context.Context, owner string, amount uint32) error { diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 37319f40..164bde2b 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/pkg/errors" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" @@ -13,7 +14,10 @@ import ( auth_util "github.com/code-payments/code-server/pkg/code/auth" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v2" + "github.com/code-payments/code-server/pkg/code/data/twitter" "github.com/code-payments/code-server/pkg/grpc/client" + timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" ) // todo: Ensure all relevant logging fields are set @@ -140,13 +144,83 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR } log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + chatId, err := chat.GetChatIdFromProto(req.ChatId) + if err != nil { + log.WithError(err).Warn("invalid chat id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("chat_id", chatId.String()) + + memberId, err := chat.GetMemberIdFromProto(req.Pointer.MemberId) + if err != nil { + log.WithError(err).Warn("invalid member id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("member_id", memberId.String()) + + pointerType := chat.GetPointerTypeFromProto(req.Pointer.Kind) + log = log.WithField("pointer_type", pointerType.String()) + switch pointerType { + case chat.PointerTypeDelivered, chat.PointerTypeRead: + default: + return nil, status.Error(codes.Unimplemented, "todo: missing result code") + } + + pointerValue, err := chat.GetMessageIdFromProto(req.Pointer.Value) + if err != nil { + log.WithError(err).Warn("invalid pointer value") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("pointer_value", pointerValue.String()) + signature := req.Signature req.Signature = nil if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { return nil, err } - return nil, status.Error(codes.Unimplemented, "") + _, err = s.data.GetChatByIdV2(ctx, chatId) + switch err { + case nil: + case chat.ErrChatNotFound: + return &chatpb.AdvancePointerResponse{ + Result: chatpb.AdvancePointerResponse_CHAT_NOT_FOUND, + }, nil + default: + log.WithError(err).Warn("failure getting chat record") + return nil, status.Error(codes.Internal, "") + } + + isChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + if err != nil { + log.WithError(err).Warn("failure determing chat member ownership") + return nil, status.Error(codes.Internal, "") + } else if !isChatMember { + return nil, status.Error(codes.Unimplemented, "todo: missing result code") + } + + _, err = s.data.GetChatMessageByIdV2(ctx, chatId, pointerValue) + switch err { + case nil: + case chat.ErrMessageNotFound: + return &chatpb.AdvancePointerResponse{ + Result: chatpb.AdvancePointerResponse_MESSAGE_NOT_FOUND, + }, nil + default: + log.WithError(err).Warn("failure getting chat message record") + return nil, status.Error(codes.Internal, "") + } + + // Note: Guarantees that pointer will never be advanced to some point in the past + err = s.data.AdvanceChatPointerV2(ctx, chatId, memberId, pointerType, pointerValue) + if err != nil { + log.WithError(err).Warn("failure advancing chat pointer") + return nil, status.Error(codes.Internal, "") + } + + return &chatpb.AdvancePointerResponse{ + Result: chatpb.AdvancePointerResponse_OK, + }, nil } func (s *server) SetMuteState(ctx context.Context, req *chatpb.SetMuteStateRequest) (*chatpb.SetMuteStateResponse, error) { @@ -160,13 +234,56 @@ func (s *server) SetMuteState(ctx context.Context, req *chatpb.SetMuteStateReque } log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + chatId, err := chat.GetChatIdFromProto(req.ChatId) + if err != nil { + log.WithError(err).Warn("invalid chat id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("chat_id", chatId.String()) + + memberId, err := chat.GetMemberIdFromProto(req.MemberId) + if err != nil { + log.WithError(err).Warn("invalid member id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("member_id", memberId.String()) + signature := req.Signature req.Signature = nil if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { return nil, err } - return nil, status.Error(codes.Unimplemented, "") + // todo: Use chat record to determine if muting is allowed + _, err = s.data.GetChatByIdV2(ctx, chatId) + switch err { + case nil: + case chat.ErrChatNotFound: + return &chatpb.SetMuteStateResponse{ + Result: chatpb.SetMuteStateResponse_CHAT_NOT_FOUND, + }, nil + default: + log.WithError(err).Warn("failure getting chat record") + return nil, status.Error(codes.Internal, "") + } + + isChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + if err != nil { + log.WithError(err).Warn("failure determing chat member ownership") + return nil, status.Error(codes.Internal, "") + } else if !isChatMember { + return nil, status.Error(codes.Unimplemented, "todo: missing result code") + } + + err = s.data.SetChatMuteStateV2(ctx, chatId, memberId, req.IsMuted) + if err != nil { + log.WithError(err).Warn("failure setting mute state") + return nil, status.Error(codes.Internal, "") + } + + return &chatpb.SetMuteStateResponse{ + Result: chatpb.SetMuteStateResponse_OK, + }, nil } func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscriptionStateRequest) (*chatpb.SetSubscriptionStateResponse, error) { @@ -180,11 +297,90 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr } log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + chatId, err := chat.GetChatIdFromProto(req.ChatId) + if err != nil { + log.WithError(err).Warn("invalid chat id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("chat_id", chatId.String()) + + memberId, err := chat.GetMemberIdFromProto(req.MemberId) + if err != nil { + log.WithError(err).Warn("invalid member id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("member_id", memberId.String()) + signature := req.Signature req.Signature = nil if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { return nil, err } - return nil, status.Error(codes.Unimplemented, "") + // todo: Use chat record to determine if muting is allowed + _, err = s.data.GetChatByIdV2(ctx, chatId) + switch err { + case nil: + case chat.ErrChatNotFound: + return &chatpb.SetSubscriptionStateResponse{ + Result: chatpb.SetSubscriptionStateResponse_CHAT_NOT_FOUND, + }, nil + default: + log.WithError(err).Warn("failure getting chat record") + return nil, status.Error(codes.Internal, "") + } + + ownsChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + if err != nil { + log.WithError(err).Warn("failure determing chat member ownership") + return nil, status.Error(codes.Internal, "") + } else if !ownsChatMember { + return nil, status.Error(codes.Unimplemented, "todo: missing result code") + } + + err = s.data.SetChatSubscriptionStateV2(ctx, chatId, memberId, req.IsSubscribed) + if err != nil { + log.WithError(err).Warn("failure setting mute state") + return nil, status.Error(codes.Internal, "") + } + + return &chatpb.SetSubscriptionStateResponse{ + Result: chatpb.SetSubscriptionStateResponse_OK, + }, nil +} + +func (s *server) ownsChatMember(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, owner *common.Account) (bool, error) { + memberRecord, err := s.data.GetChatMemberByIdV2(ctx, chatId, memberId) + switch err { + case nil: + case chat.ErrMemberNotFound: + return false, nil + default: + return false, errors.Wrap(err, "error getting member record") + } + + switch memberRecord.Platform { + case chat.PlatformCode: + return memberRecord.PlatformId == owner.PublicKey().ToBase58(), nil + case chat.PlatformTwitter: + // todo: This logic should live elsewhere in somewhere more common + + ownerTipAccount, err := owner.ToTimelockVault(timelock_token.DataVersion1, common.KinMintAccount) + if err != nil { + return false, errors.Wrap(err, "error deriving twitter tip address") + } + + twitterRecord, err := s.data.GetTwitterUserByUsername(ctx, memberRecord.PlatformId) + switch err { + case nil: + case twitter.ErrUserNotFound: + return false, nil + default: + return false, errors.Wrap(err, "error getting twitter user") + } + + return twitterRecord.TipAddress == ownerTipAccount.PublicKey().ToBase58(), nil + default: + return false, nil + } } From f0f1b917072f3d22a69589329eac06a4a894d280 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 7 Jun 2024 13:49:33 -0400 Subject: [PATCH 14/40] Implement StreamChatEvents with integration of pointer events --- pkg/code/server/grpc/chat/v2/server.go | 163 +++++++++++++++++++++++-- pkg/code/server/grpc/chat/v2/stream.go | 144 ++++++++++++++++++++++ 2 files changed, 298 insertions(+), 9 deletions(-) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 164bde2b..a8f0592a 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -2,14 +2,19 @@ package chat_v2 import ( "context" + "fmt" + "sync" "time" "github.com/pkg/errors" "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/timestamppb" chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v2" + commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" auth_util "github.com/code-payments/code-server/pkg/code/auth" "github.com/code-payments/code-server/pkg/code/common" @@ -18,6 +23,7 @@ import ( "github.com/code-payments/code-server/pkg/code/data/twitter" "github.com/code-payments/code-server/pkg/grpc/client" timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" + sync_util "github.com/code-payments/code-server/pkg/sync" ) // todo: Ensure all relevant logging fields are set @@ -27,15 +33,33 @@ type server struct { data code_data.Provider auth *auth_util.RPCSignatureVerifier + streamsMu sync.RWMutex + streams map[string]*chatEventStream + + chatLocks *sync_util.StripedLock + chatEventChans *sync_util.StripedChannel + chatpb.UnimplementedChatServer } func NewChatServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier) chatpb.ChatServer { - return &server{ - log: logrus.StandardLogger().WithField("type", "chat/v2/server"), + s := &server{ + log: logrus.StandardLogger().WithField("type", "chat/v2/server"), + data: data, auth: auth, + + streams: make(map[string]*chatEventStream), + + chatLocks: sync_util.NewStripedLock(64), // todo: configurable parameters + chatEventChans: sync_util.NewStripedChannel(64, 100_000), // todo: configurable parameters + } + + for i, channel := range s.chatEventChans.GetChannels() { + go s.asyncChatEventStreamNotifier(i, channel) } + + return s } func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*chatpb.GetChatsResponse, error) { @@ -93,10 +117,6 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e return status.Error(codes.InvalidArgument, "open_stream is nil") } - if req.GetOpenStream().Signature == nil { - return status.Error(codes.InvalidArgument, "signature is nil") - } - owner, err := common.NewAccountFromProto(req.GetOpenStream().Owner) if err != nil { log.WithError(err).Warn("invalid owner account") @@ -104,13 +124,129 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e } log = log.WithField("owner", owner.PublicKey().ToBase58()) + chatId, err := chat.GetChatIdFromProto(req.GetOpenStream().ChatId) + if err != nil { + log.WithError(err).Warn("invalid chat id") + return status.Error(codes.Internal, "") + } + log = log.WithField("chat_id", chatId.String()) + + memberId, err := chat.GetMemberIdFromProto(req.GetOpenStream().MemberId) + if err != nil { + log.WithError(err).Warn("invalid member id") + return status.Error(codes.Internal, "") + } + log = log.WithField("member_id", memberId.String()) + signature := req.GetOpenStream().Signature req.GetOpenStream().Signature = nil if err = s.auth.Authenticate(streamer.Context(), owner, req.GetOpenStream(), signature); err != nil { return err } - return status.Error(codes.Unimplemented, "") + _, err = s.data.GetChatByIdV2(ctx, chatId) + switch err { + case nil: + case chat.ErrChatNotFound: + return status.Error(codes.Unimplemented, "todo: missing result code") + default: + log.WithError(err).Warn("failure getting chat record") + return status.Error(codes.Internal, "") + } + + ownsChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + if err != nil { + log.WithError(err).Warn("failure determing chat member ownership") + return status.Error(codes.Internal, "") + } else if !ownsChatMember { + return status.Error(codes.Unimplemented, "todo: missing result code") + } + + streamKey := fmt.Sprintf("%s:%s", chatId.String(), memberId.String()) + + s.streamsMu.Lock() + + stream, exists := s.streams[streamKey] + if exists { + s.streamsMu.Unlock() + // There's an existing stream on this server that must be terminated first. + // Warn to see how often this happens in practice + log.Warnf("existing stream detected on this server (stream=%p) ; aborting", stream) + return status.Error(codes.Aborted, "stream already exists") + } + + stream = newChatEventStream(streamBufferSize) + + // The race detector complains when reading the stream pointer ref outside of the lock. + streamRef := fmt.Sprintf("%p", stream) + log.Tracef("setting up new stream (stream=%s)", streamRef) + s.streams[streamKey] = stream + + s.streamsMu.Unlock() + + defer func() { + s.streamsMu.Lock() + + log.Tracef("closing streamer (stream=%s)", streamRef) + + // We check to see if the current active stream is the one that we created. + // If it is, we can just remove it since it's closed. Otherwise, we leave it + // be, as another StreamChatEvents() call is handling it. + liveStream, exists := s.streams[streamKey] + if exists && liveStream == stream { + delete(s.streams, streamKey) + } + + s.streamsMu.Unlock() + }() + + sendPingCh := time.After(0) + streamHealthCh := monitorChatEventStreamHealth(ctx, log, streamRef, streamer) + + for { + select { + case event, ok := <-stream.streamCh: + if !ok { + log.Tracef("stream closed ; ending stream (stream=%s)", streamRef) + return status.Error(codes.Aborted, "stream closed") + } + + err := streamer.Send(&chatpb.StreamChatEventsResponse{ + Type: &chatpb.StreamChatEventsResponse_Events{ + Events: &chatpb.ChatStreamEventBatch{ + Events: []*chatpb.ChatStreamEvent{event}, + }, + }, + }) + if err != nil { + log.WithError(err).Info("failed to forward chat message") + return err + } + case <-sendPingCh: + log.Tracef("sending ping to client (stream=%s)", streamRef) + + sendPingCh = time.After(streamPingDelay) + + err := streamer.Send(&chatpb.StreamChatEventsResponse{ + Type: &chatpb.StreamChatEventsResponse_Ping{ + Ping: &commonpb.ServerPing{ + Timestamp: timestamppb.Now(), + PingDelay: durationpb.New(streamPingDelay), + }, + }, + }) + if err != nil { + log.Tracef("stream is unhealthy ; aborting (stream=%s)", streamRef) + return status.Error(codes.Aborted, "terminating unhealthy stream") + } + case <-streamHealthCh: + log.Tracef("stream is unhealthy ; aborting (stream=%s)", streamRef) + return status.Error(codes.Aborted, "terminating unhealthy stream") + case <-ctx.Done(): + log.Tracef("stream context cancelled ; ending stream (stream=%s)", streamRef) + return status.Error(codes.Canceled, "") + } + } } func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest) (*chatpb.SendMessageResponse, error) { @@ -191,11 +327,11 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR return nil, status.Error(codes.Internal, "") } - isChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + ownsChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) if err != nil { log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") - } else if !isChatMember { + } else if !ownsChatMember { return nil, status.Error(codes.Unimplemented, "todo: missing result code") } @@ -218,6 +354,15 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR return nil, status.Error(codes.Internal, "") } + event := &chatpb.ChatStreamEvent{ + Type: &chatpb.ChatStreamEvent_Pointer{ + Pointer: req.Pointer, + }, + } + if err := s.asyncNotifyAll(chatId, memberId, event); err != nil { + log.WithError(err).Warn("failure notifying chat event") + } + return &chatpb.AdvancePointerResponse{ Result: chatpb.AdvancePointerResponse_OK, }, nil diff --git a/pkg/code/server/grpc/chat/v2/stream.go b/pkg/code/server/grpc/chat/v2/stream.go index bf986572..ec797a77 100644 --- a/pkg/code/server/grpc/chat/v2/stream.go +++ b/pkg/code/server/grpc/chat/v2/stream.go @@ -2,14 +2,75 @@ package chat_v2 import ( "context" + "strings" + "sync" "time" + "github.com/pkg/errors" + "github.com/sirupsen/logrus" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v2" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v2" ) +const ( + // todo: configurable + streamBufferSize = 64 + streamPingDelay = 5 * time.Second + streamKeepAliveRecvTimeout = 10 * time.Second + streamNotifyTimeout = time.Second +) + +type chatEventStream struct { + sync.Mutex + + closed bool + streamCh chan *chatpb.ChatStreamEvent +} + +func newChatEventStream(bufferSize int) *chatEventStream { + return &chatEventStream{ + streamCh: make(chan *chatpb.ChatStreamEvent, bufferSize), + } +} + +func (s *chatEventStream) notify(event *chatpb.ChatStreamEvent, timeout time.Duration) error { + m := proto.Clone(event).(*chatpb.ChatStreamEvent) + + s.Lock() + + if s.closed { + s.Unlock() + return errors.New("cannot notify closed stream") + } + + select { + case s.streamCh <- m: + case <-time.After(timeout): + s.Unlock() + s.close() + return errors.New("timed out sending message to streamCh") + } + + s.Unlock() + return nil +} + +func (s *chatEventStream) close() { + s.Lock() + defer s.Unlock() + + if s.closed { + return + } + + s.closed = true + close(s.streamCh) +} + func boundedStreamChatEventsRecv( ctx context.Context, streamer chatpb.Chat_StreamChatEventsServer, @@ -30,3 +91,86 @@ func boundedStreamChatEventsRecv( return nil, status.Error(codes.DeadlineExceeded, "timed out receiving message") } } + +type chatEventNotification struct { + chatId chat.ChatId + memberId chat.MemberId + event *chatpb.ChatStreamEvent + ts time.Time +} + +func (s *server) asyncNotifyAll(chatId chat.ChatId, memberId chat.MemberId, event *chatpb.ChatStreamEvent) error { + m := proto.Clone(event).(*chatpb.ChatStreamEvent) + ok := s.chatEventChans.Send(chatId[:], &chatEventNotification{chatId, memberId, m, time.Now()}) + if !ok { + return errors.New("chat event channel is full") + } + return nil +} + +func (s *server) asyncChatEventStreamNotifier(workerId int, channel <-chan interface{}) { + log := s.log.WithFields(logrus.Fields{ + "method": "asyncChatEventStreamNotifier", + "worker": workerId, + }) + + for value := range channel { + typedValue, ok := value.(*chatEventNotification) + if !ok { + log.Warn("channel did not receive expected struct") + continue + } + + log := log.WithField("chat_id", typedValue.chatId.String()) + + if time.Since(typedValue.ts) > time.Second { + log.Warn("channel notification latency is elevated") + } + + s.streamsMu.RLock() + for key, stream := range s.streams { + if !strings.HasPrefix(key, typedValue.chatId.String()) { + continue + } + + if strings.HasSuffix(key, typedValue.memberId.String()) { + continue + } + + if err := stream.notify(typedValue.event, streamNotifyTimeout); err != nil { + log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) + } + } + s.streamsMu.RUnlock() + } +} + +// Very naive implementation to start +func monitorChatEventStreamHealth( + ctx context.Context, + log *logrus.Entry, + ssRef string, + streamer chatpb.Chat_StreamChatEventsServer, +) <-chan struct{} { + streamHealthChan := make(chan struct{}) + go func() { + defer close(streamHealthChan) + + for { + // todo: configurable timeout + req, err := boundedStreamChatEventsRecv(ctx, streamer, streamKeepAliveRecvTimeout) + if err != nil { + return + } + + switch req.Type.(type) { + case *chatpb.StreamChatEventsRequest_Pong: + log.Tracef("received pong from client (stream=%s)", ssRef) + default: + // Client sent something unexpected. Terminate the stream + return + } + } + }() + return streamHealthChan +} From c491d724033757123b08de2b72f7bf1dbd6a3f01 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 7 Jun 2024 14:07:21 -0400 Subject: [PATCH 15/40] Implement GetMessages RPC --- pkg/code/data/chat/v2/store.go | 7 ++ pkg/code/data/internal.go | 8 ++ pkg/code/server/grpc/chat/v2/server.go | 129 ++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 1 deletion(-) diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index b1d8d089..2abb34e1 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -3,6 +3,8 @@ package chat_v2 import ( "context" "errors" + + "github.com/code-payments/code-server/pkg/database/query" ) var ( @@ -22,6 +24,11 @@ type Store interface { // GetMessageById gets a chat message by the chat and message IDs GetMessageById(ctx context.Context, chatId ChatId, messageId MessageId) (*MessageRecord, error) + // GetAllMessagesByChat gets all messages for a given chat + // + // Note: Cursor is a message ID + GetAllMessagesByChat(ctx context.Context, chatId ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*MessageRecord, error) + // AdvancePointer advances a chat pointer for a chat member AdvancePointer(ctx context.Context, chatId ChatId, memberId MemberId, pointerType PointerType, pointer MessageId) error diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 20a72601..d7c35b71 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -398,6 +398,7 @@ type DatabaseData interface { GetChatByIdV2(ctx context.Context, chatId chat_v2.ChatId) (*chat_v2.ChatRecord, error) GetChatMemberByIdV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId) (*chat_v2.MemberRecord, error) GetChatMessageByIdV2(ctx context.Context, chatId chat_v2.ChatId, messageId chat_v2.MessageId) (*chat_v2.MessageRecord, error) + GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) error SetChatMuteStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isMuted bool) error SetChatSubscriptionStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isSubscribed bool) error @@ -1467,6 +1468,13 @@ func (dp *DatabaseProvider) GetChatMemberByIdV2(ctx context.Context, chatId chat func (dp *DatabaseProvider) GetChatMessageByIdV2(ctx context.Context, chatId chat_v2.ChatId, messageId chat_v2.MessageId) (*chat_v2.MessageRecord, error) { return dp.chatv2.GetMessageById(ctx, chatId, messageId) } +func (dp *DatabaseProvider) GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) { + req, err := query.DefaultPaginationHandler(opts...) + if err != nil { + return nil, err + } + return dp.chatv2.GetAllMessagesByChat(ctx, chatId, req.Cursor, req.SortBy, req.Limit) +} func (dp *DatabaseProvider) AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) error { return dp.chatv2.AdvancePointer(ctx, chatId, memberId, pointerType, pointer) } diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index a8f0592a..5c8246e5 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -8,8 +8,10 @@ import ( "github.com/pkg/errors" "github.com/sirupsen/logrus" + "golang.org/x/text/language" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/timestamppb" @@ -21,11 +23,17 @@ import ( code_data "github.com/code-payments/code-server/pkg/code/data" chat "github.com/code-payments/code-server/pkg/code/data/chat/v2" "github.com/code-payments/code-server/pkg/code/data/twitter" + "github.com/code-payments/code-server/pkg/code/localization" + "github.com/code-payments/code-server/pkg/database/query" "github.com/code-payments/code-server/pkg/grpc/client" timelock_token "github.com/code-payments/code-server/pkg/solana/timelock/v1" sync_util "github.com/code-payments/code-server/pkg/sync" ) +const ( + maxGetMessagesPageSize = 100 +) + // todo: Ensure all relevant logging fields are set type server struct { log *logrus.Entry @@ -93,13 +101,132 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest } log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + chatId, err := chat.GetChatIdFromProto(req.ChatId) + if err != nil { + log.WithError(err).Warn("invalid chat id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("chat_id", chatId.String()) + + memberId, err := chat.GetMemberIdFromProto(req.MemberId) + if err != nil { + log.WithError(err).Warn("invalid member id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("member_id", memberId.String()) + signature := req.Signature req.Signature = nil if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { return nil, err } - return nil, status.Error(codes.Unimplemented, "") + _, err = s.data.GetChatByIdV2(ctx, chatId) + switch err { + case nil: + case chat.ErrChatNotFound: + return nil, status.Error(codes.Unimplemented, "todo: missing result code") + default: + log.WithError(err).Warn("failure getting chat record") + return nil, status.Error(codes.Internal, "") + } + + ownsChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + if err != nil { + log.WithError(err).Warn("failure determing chat member ownership") + return nil, status.Error(codes.Internal, "") + } else if !ownsChatMember { + return nil, status.Error(codes.Unimplemented, "todo: missing result code") + } + + var limit uint64 + if req.PageSize > 0 { + limit = uint64(req.PageSize) + } else { + limit = maxGetMessagesPageSize + } + if limit > maxGetMessagesPageSize { + limit = maxGetMessagesPageSize + } + + var direction query.Ordering + if req.Direction == chatpb.GetMessagesRequest_ASC { + direction = query.Ascending + } else { + direction = query.Descending + } + + var cursor query.Cursor + if req.Cursor != nil { + cursor = req.Cursor.Value + } + + messageRecords, err := s.data.GetAllChatMessagesV2( + ctx, + chatId, + query.WithCursor(cursor), + query.WithDirection(direction), + query.WithLimit(limit), + ) + if err == chat.ErrMessageNotFound { + return &chatpb.GetMessagesResponse{ + Result: chatpb.GetMessagesResponse_NOT_FOUND, + }, nil + } else if err != nil { + log.WithError(err).Warn("failure getting chat message records") + return nil, status.Error(codes.Internal, "") + } + + var userLocale *language.Tag // Loaded lazily when required + var protoChatMessages []*chatpb.ChatMessage + for _, messageRecord := range messageRecords { + log := log.WithField("message_id", messageRecord.MessageId.String()) + + var protoChatMessage chatpb.ChatMessage + err = proto.Unmarshal(messageRecord.Data, &protoChatMessage) + if err != nil { + log.WithError(err).Warn("failure unmarshalling proto chat message") + return nil, status.Error(codes.Internal, "") + } + + ts, err := messageRecord.GetTimestamp() + if err != nil { + log.WithError(err).Warn("failure getting message timestamp") + return nil, status.Error(codes.Internal, "") + } + + for _, content := range protoChatMessage.Content { + switch typed := content.Type.(type) { + case *chatpb.Content_Localized: + if userLocale == nil { + loadedUserLocale, err := s.data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) + if err != nil { + log.WithError(err).Warn("failure getting user locale") + return nil, status.Error(codes.Internal, "") + } + userLocale = &loadedUserLocale + } + + typed.Localized.KeyOrText = localization.LocalizeWithFallback( + *userLocale, + localization.GetLocalizationKeyForUserAgent(ctx, typed.Localized.KeyOrText), + typed.Localized.KeyOrText, + ) + } + } + + protoChatMessage.MessageId = &chatpb.ChatMessageId{Value: messageRecord.MessageId[:]} + if messageRecord.Sender != nil { + protoChatMessage.SenderId = &chatpb.ChatMemberId{Value: messageRecord.Sender[:]} + } + protoChatMessage.Ts = timestamppb.New(ts) + protoChatMessage.Cursor = &chatpb.Cursor{Value: messageRecord.MessageId[:]} + } + + return &chatpb.GetMessagesResponse{ + Result: chatpb.GetMessagesResponse_OK, + Messages: protoChatMessages, + }, nil } func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) error { From 9e6b74e7f4d771e1bf3ce83059c9cce4d97f4035 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Fri, 7 Jun 2024 14:08:25 -0400 Subject: [PATCH 16/40] Add reminder to add a flush on StreamChatEvents --- pkg/code/server/grpc/chat/v2/server.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 5c8246e5..fdf3b4fd 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -229,6 +229,7 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest }, nil } +// todo: Requires a "flush" of most recent messages func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) error { ctx := streamer.Context() From a18a7254f7762363888193e126ff7688ab1be8bf Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 10 Jun 2024 09:46:44 -0400 Subject: [PATCH 17/40] Add a flush on StreamChatEvents stream open --- pkg/code/server/grpc/chat/v2/server.go | 146 ++++++++++++++++--------- 1 file changed, 92 insertions(+), 54 deletions(-) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index fdf3b4fd..eed7ec8c 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -161,75 +161,25 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest cursor = req.Cursor.Value } - messageRecords, err := s.data.GetAllChatMessagesV2( + protoChatMessages, err := s.getProtoChatMessages( ctx, chatId, + owner, query.WithCursor(cursor), query.WithDirection(direction), query.WithLimit(limit), ) - if err == chat.ErrMessageNotFound { - return &chatpb.GetMessagesResponse{ - Result: chatpb.GetMessagesResponse_NOT_FOUND, - }, nil - } else if err != nil { - log.WithError(err).Warn("failure getting chat message records") + if err != nil { + log.WithError(err).Warn("failure getting chat messages") return nil, status.Error(codes.Internal, "") } - var userLocale *language.Tag // Loaded lazily when required - var protoChatMessages []*chatpb.ChatMessage - for _, messageRecord := range messageRecords { - log := log.WithField("message_id", messageRecord.MessageId.String()) - - var protoChatMessage chatpb.ChatMessage - err = proto.Unmarshal(messageRecord.Data, &protoChatMessage) - if err != nil { - log.WithError(err).Warn("failure unmarshalling proto chat message") - return nil, status.Error(codes.Internal, "") - } - - ts, err := messageRecord.GetTimestamp() - if err != nil { - log.WithError(err).Warn("failure getting message timestamp") - return nil, status.Error(codes.Internal, "") - } - - for _, content := range protoChatMessage.Content { - switch typed := content.Type.(type) { - case *chatpb.Content_Localized: - if userLocale == nil { - loadedUserLocale, err := s.data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) - if err != nil { - log.WithError(err).Warn("failure getting user locale") - return nil, status.Error(codes.Internal, "") - } - userLocale = &loadedUserLocale - } - - typed.Localized.KeyOrText = localization.LocalizeWithFallback( - *userLocale, - localization.GetLocalizationKeyForUserAgent(ctx, typed.Localized.KeyOrText), - typed.Localized.KeyOrText, - ) - } - } - - protoChatMessage.MessageId = &chatpb.ChatMessageId{Value: messageRecord.MessageId[:]} - if messageRecord.Sender != nil { - protoChatMessage.SenderId = &chatpb.ChatMemberId{Value: messageRecord.Sender[:]} - } - protoChatMessage.Ts = timestamppb.New(ts) - protoChatMessage.Cursor = &chatpb.Cursor{Value: messageRecord.MessageId[:]} - } - return &chatpb.GetMessagesResponse{ Result: chatpb.GetMessagesResponse_OK, Messages: protoChatMessages, }, nil } -// todo: Requires a "flush" of most recent messages func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) error { ctx := streamer.Context() @@ -331,6 +281,8 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e sendPingCh := time.After(0) streamHealthCh := monitorChatEventStreamHealth(ctx, log, streamRef, streamer) + go s.flush(ctx, chatId, owner, stream) + for { select { case event, ok := <-stream.streamCh: @@ -622,6 +574,92 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr }, nil } +func (s *server) flush(ctx context.Context, chatId chat.ChatId, owner *common.Account, stream *chatEventStream) { + log := s.log.WithFields(logrus.Fields{ + "method": "flush", + "chat_id": chatId.String(), + "owner_account": owner.PublicKey().ToBase58(), + }) + + protoChatMessages, err := s.getProtoChatMessages( + ctx, + chatId, + owner, + ) + if err != nil { + log.WithError(err).Warn("failure getting chat messages") + return + } + + for _, protoChatMessage := range protoChatMessages { + event := &chatpb.ChatStreamEvent{ + Type: &chatpb.ChatStreamEvent_Message{ + Message: protoChatMessage, + }, + } + if err := stream.notify(event, streamNotifyTimeout); err != nil { + log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) + return + } + } +} + +func (s *server) getProtoChatMessages(ctx context.Context, chatId chat.ChatId, owner *common.Account, queryOptions ...query.Option) ([]*chatpb.ChatMessage, error) { + messageRecords, err := s.data.GetAllChatMessagesV2( + ctx, + chatId, + queryOptions..., + ) + if err == chat.ErrMessageNotFound { + return nil, err + } + + var userLocale *language.Tag // Loaded lazily when required + var res []*chatpb.ChatMessage + for _, messageRecord := range messageRecords { + var protoChatMessage chatpb.ChatMessage + err = proto.Unmarshal(messageRecord.Data, &protoChatMessage) + if err != nil { + return nil, errors.Wrap(err, "error unmarshalling proto chat message") + } + + ts, err := messageRecord.GetTimestamp() + if err != nil { + return nil, errors.Wrap(err, "error getting message timestamp") + } + + for _, content := range protoChatMessage.Content { + switch typed := content.Type.(type) { + case *chatpb.Content_Localized: + if userLocale == nil { + loadedUserLocale, err := s.data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) + if err != nil { + return nil, errors.Wrap(err, "error getting user locale") + } + userLocale = &loadedUserLocale + } + + typed.Localized.KeyOrText = localization.LocalizeWithFallback( + *userLocale, + localization.GetLocalizationKeyForUserAgent(ctx, typed.Localized.KeyOrText), + typed.Localized.KeyOrText, + ) + } + } + + protoChatMessage.MessageId = &chatpb.ChatMessageId{Value: messageRecord.MessageId[:]} + if messageRecord.Sender != nil { + protoChatMessage.SenderId = &chatpb.ChatMemberId{Value: messageRecord.Sender[:]} + } + protoChatMessage.Ts = timestamppb.New(ts) + protoChatMessage.Cursor = &chatpb.Cursor{Value: messageRecord.MessageId[:]} + + res = append(res, &protoChatMessage) + } + + return res, nil +} + func (s *server) ownsChatMember(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, owner *common.Account) (bool, error) { memberRecord, err := s.data.GetChatMemberByIdV2(ctx, chatId, memberId) switch err { From ccda13ee082e9063ec9bd9fc6f1a93f66b4c399a Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 10 Jun 2024 09:48:26 -0400 Subject: [PATCH 18/40] Fix todo comment --- pkg/code/server/grpc/chat/v2/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index eed7ec8c..c8f7459a 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -542,7 +542,7 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr return nil, err } - // todo: Use chat record to determine if muting is allowed + // todo: Use chat record to determine if unsubscribing is allowed _, err = s.data.GetChatByIdV2(ctx, chatId) switch err { case nil: From d6f3e54d7fff31a65fb8de95cbdb9126a1bc351f Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 10 Jun 2024 09:53:37 -0400 Subject: [PATCH 19/40] Add missing query parameters to flush --- pkg/code/server/grpc/chat/v2/server.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index c8f7459a..30e37b23 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -32,6 +32,7 @@ import ( const ( maxGetMessagesPageSize = 100 + flushMessageCount = 100 ) // todo: Ensure all relevant logging fields are set @@ -581,10 +582,15 @@ func (s *server) flush(ctx context.Context, chatId chat.ChatId, owner *common.Ac "owner_account": owner.PublicKey().ToBase58(), }) + cursorValue := chat.GenerateMessageIdAtTime(time.Now().Add(2 * time.Second)) + protoChatMessages, err := s.getProtoChatMessages( ctx, chatId, owner, + query.WithCursor(cursorValue[:]), + query.WithDirection(query.Descending), + query.WithLimit(flushMessageCount), ) if err != nil { log.WithError(err).Warn("failure getting chat messages") From 9740697f95a62beb8c166198a363c7ddfdc789ce Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 10 Jun 2024 10:35:29 -0400 Subject: [PATCH 20/40] Implement the SendMessage RPC without consideration for other chat features --- pkg/code/data/chat/v2/id.go | 21 ++- pkg/code/data/chat/v2/store.go | 3 + pkg/code/data/internal.go | 4 + pkg/code/server/grpc/chat/v2/server.go | 214 ++++++++++++++++++++----- 4 files changed, 200 insertions(+), 42 deletions(-) diff --git a/pkg/code/data/chat/v2/id.go b/pkg/code/data/chat/v2/id.go index 267ae7f2..b9ef4667 100644 --- a/pkg/code/data/chat/v2/id.go +++ b/pkg/code/data/chat/v2/id.go @@ -29,6 +29,11 @@ func GetChatIdFromProto(proto *chatpb.ChatId) (ChatId, error) { return typed, nil } +// ToProto converts a chat ID to its protobuf variant +func (c ChatId) ToProto() *chatpb.ChatId { + return &chatpb.ChatId{Value: c[:]} +} + // Validate validates a chat ID func (c ChatId) Validate() error { return nil @@ -42,6 +47,11 @@ func (c ChatId) String() string { // Random UUIDv4 ID for chat members type MemberId uuid.UUID +// GenerateMemberId generates a new random chat member ID +func GenerateMemberId() MemberId { + return MemberId(uuid.New()) +} + // GetMemberIdFromProto gets a member ID from the protobuf variant func GetMemberIdFromProto(proto *chatpb.ChatMemberId) (MemberId, error) { if err := proto.Validate(); err != nil { @@ -58,9 +68,9 @@ func GetMemberIdFromProto(proto *chatpb.ChatMemberId) (MemberId, error) { return typed, nil } -// GenerateMemberId generates a new random chat member ID -func GenerateMemberId() MemberId { - return MemberId(uuid.New()) +// ToProto converts a message ID to its protobuf variant +func (m MemberId) ToProto() *chatpb.ChatMemberId { + return &chatpb.ChatMemberId{Value: m[:]} } // Validate validates a chat member ID @@ -129,6 +139,11 @@ func GetMessageIdFromProto(proto *chatpb.ChatMessageId) (MessageId, error) { return typed, nil } +// ToProto converts a message ID to its protobuf variant +func (m MessageId) ToProto() *chatpb.ChatMessageId { + return &chatpb.ChatMessageId{Value: m[:]} +} + // GetTimestamp gets the encoded timestamp in the message ID func (m MessageId) GetTimestamp() (time.Time, error) { if err := m.Validate(); err != nil { diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index 2abb34e1..54e00932 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -29,6 +29,9 @@ type Store interface { // Note: Cursor is a message ID GetAllMessagesByChat(ctx context.Context, chatId ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*MessageRecord, error) + // PutMessage creates a new chat message + PutMessage(ctx context.Context, record *MessageRecord) error + // AdvancePointer advances a chat pointer for a chat member AdvancePointer(ctx context.Context, chatId ChatId, memberId MemberId, pointerType PointerType, pointer MessageId) error diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index d7c35b71..f2d1af0c 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -399,6 +399,7 @@ type DatabaseData interface { GetChatMemberByIdV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId) (*chat_v2.MemberRecord, error) GetChatMessageByIdV2(ctx context.Context, chatId chat_v2.ChatId, messageId chat_v2.MessageId) (*chat_v2.MessageRecord, error) GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) + PutChatMessageV2(ctx context.Context, record *chat_v2.MessageRecord) error AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) error SetChatMuteStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isMuted bool) error SetChatSubscriptionStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isSubscribed bool) error @@ -1475,6 +1476,9 @@ func (dp *DatabaseProvider) GetAllChatMessagesV2(ctx context.Context, chatId cha } return dp.chatv2.GetAllMessagesByChat(ctx, chatId, req.Cursor, req.SortBy, req.Limit) } +func (dp *DatabaseProvider) PutChatMessageV2(ctx context.Context, record *chat_v2.MessageRecord) error { + return dp.chatv2.PutMessage(ctx, record) +} func (dp *DatabaseProvider) AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) error { return dp.chatv2.AdvancePointer(ctx, chatId, memberId, pointerType, pointer) } diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 30e37b23..b3f815fe 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -330,6 +330,41 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e } } +func (s *server) flush(ctx context.Context, chatId chat.ChatId, owner *common.Account, stream *chatEventStream) { + log := s.log.WithFields(logrus.Fields{ + "method": "flush", + "chat_id": chatId.String(), + "owner_account": owner.PublicKey().ToBase58(), + }) + + cursorValue := chat.GenerateMessageIdAtTime(time.Now().Add(2 * time.Second)) + + protoChatMessages, err := s.getProtoChatMessages( + ctx, + chatId, + owner, + query.WithCursor(cursorValue[:]), + query.WithDirection(query.Descending), + query.WithLimit(flushMessageCount), + ) + if err != nil { + log.WithError(err).Warn("failure getting chat messages") + return + } + + for _, protoChatMessage := range protoChatMessages { + event := &chatpb.ChatStreamEvent{ + Type: &chatpb.ChatStreamEvent_Message{ + Message: protoChatMessage, + }, + } + if err := stream.notify(event, streamNotifyTimeout); err != nil { + log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) + return + } + } +} + func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest) (*chatpb.SendMessageResponse, error) { log := s.log.WithField("method", "SendMessage") log = client.InjectLoggingMetadata(ctx, log) @@ -339,7 +374,29 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest log.WithError(err).Warn("invalid owner account") return nil, status.Error(codes.Internal, "") } - log = log.WithField("owner", owner.PublicKey().ToBase58()) + log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + + chatId, err := chat.GetChatIdFromProto(req.ChatId) + if err != nil { + log.WithError(err).Warn("invalid chat id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("chat_id", chatId.String()) + + memberId, err := chat.GetMemberIdFromProto(req.MemberId) + if err != nil { + log.WithError(err).Warn("invalid member id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("member_id", memberId.String()) + + switch req.Content[0].Type.(type) { + case *chatpb.Content_Text, *chatpb.Content_ThankYou: + default: + return &chatpb.SendMessageResponse{ + Result: chatpb.SendMessageResponse_INVALID_CONTENT_TYPE, + }, nil + } signature := req.Signature req.Signature = nil @@ -347,7 +404,121 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest return nil, err } - return nil, status.Error(codes.Unimplemented, "") + chatRecord, err := s.data.GetChatByIdV2(ctx, chatId) + switch err { + case nil: + case chat.ErrChatNotFound: + return &chatpb.SendMessageResponse{ + Result: chatpb.SendMessageResponse_CHAT_NOT_FOUND, + }, nil + default: + log.WithError(err).Warn("failure getting chat record") + return nil, status.Error(codes.Internal, "") + } + + switch chatRecord.ChatType { + case chat.ChatTypeTwoWay: + default: + return &chatpb.SendMessageResponse{ + Result: chatpb.SendMessageResponse_INVALID_CHAT_TYPE, + }, nil + } + + ownsChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + if err != nil { + log.WithError(err).Warn("failure determing chat member ownership") + return nil, status.Error(codes.Internal, "") + } else if !ownsChatMember { + return nil, status.Error(codes.Unimplemented, "todo: missing result code") + } + + chatLock := s.chatLocks.Get(chatId[:]) + chatLock.Lock() + defer chatLock.Unlock() + + messageId := chat.GenerateMessageId() + ts, _ := messageId.GetTimestamp() + + chatMessage := &chatpb.ChatMessage{ + MessageId: messageId.ToProto(), + SenderId: req.MemberId, + Content: req.Content, + Ts: timestamppb.New(ts), + Cursor: &chatpb.Cursor{Value: messageId[:]}, + } + + err = s.persistChatMessage(ctx, chatId, chatMessage) + if err != nil { + log.WithError(err).Warn("failure persisting chat message") + return nil, status.Error(codes.Internal, "") + } + + event := &chatpb.ChatStreamEvent{ + Type: &chatpb.ChatStreamEvent_Message{ + Message: chatMessage, + }, + } + if err := s.asyncNotifyAll(chatId, memberId, event); err != nil { + log.WithError(err).Warn("failure notifying chat event") + } + + // todo: send the push + + return &chatpb.SendMessageResponse{ + Result: chatpb.SendMessageResponse_OK, + Message: chatMessage, + }, nil +} + +// todo: This belongs in the common chat utility, which currently only operates on v1 chats +func (s *server) persistChatMessage(ctx context.Context, chatId chat.ChatId, protoChatMessage *chatpb.ChatMessage) error { + if err := protoChatMessage.Validate(); err != nil { + return errors.Wrap(err, "proto chat message failed validation") + } + + messageId, err := chat.GetMessageIdFromProto(protoChatMessage.MessageId) + if err != nil { + return errors.Wrap(err, "invalid message id") + } + + var senderId *chat.MemberId + if protoChatMessage.SenderId != nil { + convertedSenderId, err := chat.GetMemberIdFromProto(protoChatMessage.SenderId) + if err != nil { + return errors.Wrap(err, "invalid member id") + } + senderId = &convertedSenderId + } + + // Clear out extracted metadata as a space optimization + cloned := proto.Clone(protoChatMessage).(*chatpb.ChatMessage) + cloned.MessageId = nil + cloned.SenderId = nil + cloned.Ts = nil + cloned.Cursor = nil + + marshalled, err := proto.Marshal(cloned) + if err != nil { + return errors.Wrap(err, "error marshalling proto chat message") + } + + // todo: Doesn't incoroporate reference. We might want to promote the proto a level above the content. + messageRecord := &chat.MessageRecord{ + ChatId: chatId, + MessageId: messageId, + + Sender: senderId, + + Data: marshalled, + + IsSilent: false, + } + + err = s.data.PutChatMessageV2(ctx, messageRecord) + if err != nil { + return errors.Wrap(err, "error persiting chat message") + } + return nil } func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerRequest) (*chatpb.AdvancePointerResponse, error) { @@ -575,41 +746,6 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr }, nil } -func (s *server) flush(ctx context.Context, chatId chat.ChatId, owner *common.Account, stream *chatEventStream) { - log := s.log.WithFields(logrus.Fields{ - "method": "flush", - "chat_id": chatId.String(), - "owner_account": owner.PublicKey().ToBase58(), - }) - - cursorValue := chat.GenerateMessageIdAtTime(time.Now().Add(2 * time.Second)) - - protoChatMessages, err := s.getProtoChatMessages( - ctx, - chatId, - owner, - query.WithCursor(cursorValue[:]), - query.WithDirection(query.Descending), - query.WithLimit(flushMessageCount), - ) - if err != nil { - log.WithError(err).Warn("failure getting chat messages") - return - } - - for _, protoChatMessage := range protoChatMessages { - event := &chatpb.ChatStreamEvent{ - Type: &chatpb.ChatStreamEvent_Message{ - Message: protoChatMessage, - }, - } - if err := stream.notify(event, streamNotifyTimeout); err != nil { - log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) - return - } - } -} - func (s *server) getProtoChatMessages(ctx context.Context, chatId chat.ChatId, owner *common.Account, queryOptions ...query.Option) ([]*chatpb.ChatMessage, error) { messageRecords, err := s.data.GetAllChatMessagesV2( ctx, @@ -653,9 +789,9 @@ func (s *server) getProtoChatMessages(ctx context.Context, chatId chat.ChatId, o } } - protoChatMessage.MessageId = &chatpb.ChatMessageId{Value: messageRecord.MessageId[:]} + protoChatMessage.MessageId = messageRecord.MessageId.ToProto() if messageRecord.Sender != nil { - protoChatMessage.SenderId = &chatpb.ChatMemberId{Value: messageRecord.Sender[:]} + protoChatMessage.SenderId = messageRecord.Sender.ToProto() } protoChatMessage.Ts = timestamppb.New(ts) protoChatMessage.Cursor = &chatpb.Cursor{Value: messageRecord.MessageId[:]} From 38149d79a0f81ea229aaa4654a0e54eea766ea1f Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 10 Jun 2024 15:07:26 -0400 Subject: [PATCH 21/40] Skeleton for minimal memory PoC chat v2 data store --- pkg/code/data/chat/v2/memory/store.go | 78 ++++++++++++++++++++++ pkg/code/data/chat/v2/memory/store_test.go | 15 +++++ pkg/code/data/chat/v2/store.go | 6 ++ pkg/code/data/chat/v2/tests/tests.go | 14 ++++ pkg/code/data/internal.go | 13 +++- 5 files changed, 124 insertions(+), 2 deletions(-) create mode 100644 pkg/code/data/chat/v2/memory/store.go create mode 100644 pkg/code/data/chat/v2/memory/store_test.go create mode 100644 pkg/code/data/chat/v2/tests/tests.go diff --git a/pkg/code/data/chat/v2/memory/store.go b/pkg/code/data/chat/v2/memory/store.go new file mode 100644 index 00000000..cdd8e4b1 --- /dev/null +++ b/pkg/code/data/chat/v2/memory/store.go @@ -0,0 +1,78 @@ +package memory + +import ( + "context" + "errors" + "sync" + + chat "github.com/code-payments/code-server/pkg/code/data/chat/v2" + "github.com/code-payments/code-server/pkg/database/query" +) + +// todo: implement me +type store struct { + mu sync.Mutex + last uint64 +} + +// New returns a new in memory chat.Store +func New() chat.Store { + return &store{} +} + +// GetChatById implements chat.Store.GetChatById +func (s *store) GetChatById(ctx context.Context, chatId chat.ChatId) (*chat.ChatRecord, error) { + return nil, errors.New("not implemented") +} + +// GetMemberById implements chat.Store.GetMemberById +func (s *store) GetMemberById(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId) (*chat.MemberRecord, error) { + return nil, errors.New("not implemented") +} + +// GetMessageById implements chat.Store.GetMessageById +func (s *store) GetMessageById(ctx context.Context, chatId chat.ChatId, messageId chat.MessageId) (*chat.MessageRecord, error) { + return nil, errors.New("not implemented") +} + +// GetAllMessagesByChat implements chat.Store.GetAllMessagesByChat +func (s *store) GetAllMessagesByChat(ctx context.Context, chatId chat.ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MessageRecord, error) { + return nil, errors.New("not implemented") +} + +// PutChat creates a new chat +func (s *store) PutChat(ctx context.Context, record *chat.ChatRecord) error { + return errors.New("not implemented") +} + +// PutMember creates a new chat member +func (s *store) PutMember(ctx context.Context, record *chat.MemberRecord) error { + return errors.New("not implemented") +} + +// PutMessage implements chat.Store.PutMessage +func (s *store) PutMessage(ctx context.Context, record *chat.MessageRecord) error { + return errors.New("not implemented") +} + +// AdvancePointer implements chat.Store.AdvancePointer +func (s *store) AdvancePointer(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, pointerType chat.PointerType, pointer chat.MessageId) error { + return errors.New("not implemented") +} + +// SetMuteState implements chat.Store.SetMuteState +func (s *store) SetMuteState(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, isMuted bool) error { + return errors.New("not implemented") +} + +// SetSubscriptionState implements chat.Store.SetSubscriptionState +func (s *store) SetSubscriptionState(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, isSubscribed bool) error { + return errors.New("not implemented") +} + +func (s *store) reset() { + s.mu.Lock() + defer s.mu.Unlock() + + s.last = 0 +} diff --git a/pkg/code/data/chat/v2/memory/store_test.go b/pkg/code/data/chat/v2/memory/store_test.go new file mode 100644 index 00000000..cd61dfa4 --- /dev/null +++ b/pkg/code/data/chat/v2/memory/store_test.go @@ -0,0 +1,15 @@ +package memory + +import ( + "testing" + + "github.com/code-payments/code-server/pkg/code/data/chat/v2/tests" +) + +func TestChatMemoryStore(t *testing.T) { + testStore := New() + teardown := func() { + testStore.(*store).reset() + } + tests.RunTests(t, testStore, teardown) +} diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index 54e00932..ab73943e 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -29,6 +29,12 @@ type Store interface { // Note: Cursor is a message ID GetAllMessagesByChat(ctx context.Context, chatId ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*MessageRecord, error) + // PutChat creates a new chat + PutChat(ctx context.Context, record *ChatRecord) error + + // PutMember creates a new chat member + PutMember(ctx context.Context, record *MemberRecord) error + // PutMessage creates a new chat message PutMessage(ctx context.Context, record *MessageRecord) error diff --git a/pkg/code/data/chat/v2/tests/tests.go b/pkg/code/data/chat/v2/tests/tests.go new file mode 100644 index 00000000..94c85a89 --- /dev/null +++ b/pkg/code/data/chat/v2/tests/tests.go @@ -0,0 +1,14 @@ +package tests + +import ( + "testing" + + chat "github.com/code-payments/code-server/pkg/code/data/chat/v2" +) + +func RunTests(t *testing.T, s chat.Store, teardown func()) { + for _, tf := range []func(t *testing.T, s chat.Store){} { + tf(t, s) + teardown() + } +} diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index f2d1af0c..68cce410 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -61,6 +61,7 @@ import ( badgecount_memory_client "github.com/code-payments/code-server/pkg/code/data/badgecount/memory" balance_memory_client "github.com/code-payments/code-server/pkg/code/data/balance/memory" chat_v1_memory_client "github.com/code-payments/code-server/pkg/code/data/chat/v1/memory" + chat_v2_memory_client "github.com/code-payments/code-server/pkg/code/data/chat/v2/memory" commitment_memory_client "github.com/code-payments/code-server/pkg/code/data/commitment/memory" contact_memory_client "github.com/code-payments/code-server/pkg/code/data/contact/memory" currency_memory_client "github.com/code-payments/code-server/pkg/code/data/currency/memory" @@ -399,6 +400,8 @@ type DatabaseData interface { GetChatMemberByIdV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId) (*chat_v2.MemberRecord, error) GetChatMessageByIdV2(ctx context.Context, chatId chat_v2.ChatId, messageId chat_v2.MessageId) (*chat_v2.MessageRecord, error) GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) + PutChatV2(ctx context.Context, record *chat_v2.ChatRecord) error + PutChatMemberV2(ctx context.Context, record *chat_v2.MemberRecord) error PutChatMessageV2(ctx context.Context, record *chat_v2.MessageRecord) error AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) error SetChatMuteStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isMuted bool) error @@ -546,7 +549,7 @@ func NewDatabaseProvider(dbConfig *pg.Config) (DatabaseData, error) { event: event_postgres_client.New(db), webhook: webhook_postgres_client.New(db), chatv1: chat_v1_postgres_client.New(db), - chatv2: nil, // todo: Initialize me + chatv2: chat_v2_memory_client.New(), // todo: Postgres version for production after PoC badgecount: badgecount_postgres_client.New(db), login: login_postgres_client.New(db), balance: balance_postgres_client.New(db), @@ -590,7 +593,7 @@ func NewTestDatabaseProvider() DatabaseData { event: event_memory_client.New(), webhook: webhook_memory_client.New(), chatv1: chat_v1_memory_client.New(), - chatv2: nil, // todo: initialize me + chatv2: chat_v2_memory_client.New(), badgecount: badgecount_memory_client.New(), login: login_memory_client.New(), balance: balance_memory_client.New(), @@ -1476,6 +1479,12 @@ func (dp *DatabaseProvider) GetAllChatMessagesV2(ctx context.Context, chatId cha } return dp.chatv2.GetAllMessagesByChat(ctx, chatId, req.Cursor, req.SortBy, req.Limit) } +func (dp *DatabaseProvider) PutChatV2(ctx context.Context, record *chat_v2.ChatRecord) error { + return dp.chatv2.PutChat(ctx, record) +} +func (dp *DatabaseProvider) PutChatMemberV2(ctx context.Context, record *chat_v2.MemberRecord) error { + return dp.chatv2.PutMember(ctx, record) +} func (dp *DatabaseProvider) PutChatMessageV2(ctx context.Context, record *chat_v2.MessageRecord) error { return dp.chatv2.PutMessage(ctx, record) } From 695ee4edb4054d7c79b1f0e5faccd4f709b8c3cd Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 10 Jun 2024 15:26:00 -0400 Subject: [PATCH 22/40] Implement chat v2 memory data store (WIP) --- pkg/code/data/chat/v2/memory/store.go | 173 +++++++++++++++++++++++--- pkg/code/data/chat/v2/store.go | 7 +- 2 files changed, 157 insertions(+), 23 deletions(-) diff --git a/pkg/code/data/chat/v2/memory/store.go b/pkg/code/data/chat/v2/memory/store.go index cdd8e4b1..e3802aa0 100644 --- a/pkg/code/data/chat/v2/memory/store.go +++ b/pkg/code/data/chat/v2/memory/store.go @@ -1,6 +1,7 @@ package memory import ( + "bytes" "context" "errors" "sync" @@ -9,10 +10,17 @@ import ( "github.com/code-payments/code-server/pkg/database/query" ) -// todo: implement me +// todo: finish implementing me type store struct { - mu sync.Mutex - last uint64 + mu sync.Mutex + + chatRecords []*chat.ChatRecord + memberRecords []*chat.MemberRecord + messageRecords []*chat.MessageRecord + + lastChatId uint64 + lastMemberId uint64 + lastMessageId uint64 } // New returns a new in memory chat.Store @@ -21,58 +29,183 @@ func New() chat.Store { } // GetChatById implements chat.Store.GetChatById -func (s *store) GetChatById(ctx context.Context, chatId chat.ChatId) (*chat.ChatRecord, error) { - return nil, errors.New("not implemented") +func (s *store) GetChatById(_ context.Context, chatId chat.ChatId) (*chat.ChatRecord, error) { + s.mu.Lock() + defer s.mu.Unlock() + + item := s.findChatById(chatId) + if item == nil { + return nil, chat.ErrChatNotFound + } + + cloned := item.Clone() + return &cloned, nil } // GetMemberById implements chat.Store.GetMemberById -func (s *store) GetMemberById(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId) (*chat.MemberRecord, error) { - return nil, errors.New("not implemented") +func (s *store) GetMemberById(_ context.Context, chatId chat.ChatId, memberId chat.MemberId) (*chat.MemberRecord, error) { + s.mu.Lock() + defer s.mu.Unlock() + + item := s.findMemberById(chatId, memberId) + if item == nil { + return nil, chat.ErrMemberNotFound + } + + cloned := item.Clone() + return &cloned, nil } // GetMessageById implements chat.Store.GetMessageById -func (s *store) GetMessageById(ctx context.Context, chatId chat.ChatId, messageId chat.MessageId) (*chat.MessageRecord, error) { - return nil, errors.New("not implemented") +func (s *store) GetMessageById(_ context.Context, chatId chat.ChatId, messageId chat.MessageId) (*chat.MessageRecord, error) { + s.mu.Lock() + defer s.mu.Unlock() + + item := s.findMessageById(chatId, messageId) + if item == nil { + return nil, chat.ErrMessageNotFound + } + + cloned := item.Clone() + return &cloned, nil } // GetAllMessagesByChat implements chat.Store.GetAllMessagesByChat -func (s *store) GetAllMessagesByChat(ctx context.Context, chatId chat.ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MessageRecord, error) { +func (s *store) GetAllMessagesByChat(_ context.Context, chatId chat.ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MessageRecord, error) { + s.mu.Lock() + defer s.mu.Unlock() + return nil, errors.New("not implemented") } // PutChat creates a new chat -func (s *store) PutChat(ctx context.Context, record *chat.ChatRecord) error { +func (s *store) PutChat(_ context.Context, record *chat.ChatRecord) error { + s.mu.Lock() + defer s.mu.Unlock() + return errors.New("not implemented") } // PutMember creates a new chat member -func (s *store) PutMember(ctx context.Context, record *chat.MemberRecord) error { +func (s *store) PutMember(_ context.Context, record *chat.MemberRecord) error { + s.mu.Lock() + defer s.mu.Unlock() + return errors.New("not implemented") } // PutMessage implements chat.Store.PutMessage -func (s *store) PutMessage(ctx context.Context, record *chat.MessageRecord) error { +func (s *store) PutMessage(_ context.Context, record *chat.MessageRecord) error { + s.mu.Lock() + defer s.mu.Unlock() + return errors.New("not implemented") } // AdvancePointer implements chat.Store.AdvancePointer -func (s *store) AdvancePointer(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, pointerType chat.PointerType, pointer chat.MessageId) error { - return errors.New("not implemented") +func (s *store) AdvancePointer(_ context.Context, chatId chat.ChatId, memberId chat.MemberId, pointerType chat.PointerType, pointer chat.MessageId) error { + switch pointerType { + case chat.PointerTypeDelivered, chat.PointerTypeRead: + default: + return chat.ErrInvalidPointerType + } + + s.mu.Lock() + defer s.mu.Unlock() + + item := s.findMemberById(chatId, memberId) + if item == nil { + return chat.ErrMemberNotFound + } + + var currentPointer *chat.MessageId + switch pointerType { + case chat.PointerTypeDelivered: + currentPointer = item.DeliveryPointer + case chat.PointerTypeRead: + currentPointer = item.ReadPointer + } + + if currentPointer != nil && currentPointer.After(pointer) { + return nil + } + + switch pointerType { + case chat.PointerTypeDelivered: + item.DeliveryPointer = &pointer // todo: pointer copy safety + case chat.PointerTypeRead: + item.ReadPointer = &pointer // todo: pointer copy safety + } + + return nil } // SetMuteState implements chat.Store.SetMuteState -func (s *store) SetMuteState(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, isMuted bool) error { - return errors.New("not implemented") +func (s *store) SetMuteState(_ context.Context, chatId chat.ChatId, memberId chat.MemberId, isMuted bool) error { + s.mu.Lock() + defer s.mu.Unlock() + + item := s.findMemberById(chatId, memberId) + if item == nil { + return chat.ErrMemberNotFound + } + + item.IsMuted = isMuted + + return nil } // SetSubscriptionState implements chat.Store.SetSubscriptionState -func (s *store) SetSubscriptionState(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, isSubscribed bool) error { - return errors.New("not implemented") +func (s *store) SetSubscriptionState(_ context.Context, chatId chat.ChatId, memberId chat.MemberId, isSubscribed bool) error { + s.mu.Lock() + defer s.mu.Unlock() + + item := s.findMemberById(chatId, memberId) + if item == nil { + return chat.ErrMemberNotFound + } + + item.IsUnsubscribed = !isSubscribed + + return nil +} + +func (s *store) findChatById(chatId chat.ChatId) *chat.ChatRecord { + for _, item := range s.chatRecords { + if bytes.Equal(chatId[:], item.ChatId[:]) { + return item + } + } + return nil +} + +func (s *store) findMemberById(chatId chat.ChatId, memberId chat.MemberId) *chat.MemberRecord { + for _, item := range s.memberRecords { + if bytes.Equal(chatId[:], item.ChatId[:]) && bytes.Equal(memberId[:], item.MemberId[:]) { + return item + } + } + return nil +} + +func (s *store) findMessageById(chatId chat.ChatId, messageId chat.MessageId) *chat.MessageRecord { + for _, item := range s.messageRecords { + if bytes.Equal(chatId[:], item.ChatId[:]) && bytes.Equal(messageId[:], item.MessageId[:]) { + return item + } + } + return nil } func (s *store) reset() { s.mu.Lock() defer s.mu.Unlock() - s.last = 0 + s.chatRecords = nil + s.memberRecords = nil + s.messageRecords = nil + + s.lastChatId = 0 + s.lastMemberId = 0 + s.lastMessageId = 0 } diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index ab73943e..e7234b3a 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -8,9 +8,10 @@ import ( ) var ( - ErrChatNotFound = errors.New("chat not found") - ErrMemberNotFound = errors.New("chat member not found") - ErrMessageNotFound = errors.New("chat message not found") + ErrChatNotFound = errors.New("chat not found") + ErrMemberNotFound = errors.New("chat member not found") + ErrMessageNotFound = errors.New("chat message not found") + ErrInvalidPointerType = errors.New("invalid pointer type") ) // todo: Define interface methods From fa485f1eccc69783a1db6bb089fc881b3e660c81 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 11 Jun 2024 10:05:42 -0400 Subject: [PATCH 23/40] Implement remaining chat memory store defined methods --- pkg/code/data/chat/v2/id.go | 57 ++++++--- pkg/code/data/chat/v2/memory/store.go | 177 ++++++++++++++++++++++++-- pkg/code/data/chat/v2/model.go | 8 ++ pkg/code/data/chat/v2/store.go | 3 + 4 files changed, 222 insertions(+), 23 deletions(-) diff --git a/pkg/code/data/chat/v2/id.go b/pkg/code/data/chat/v2/id.go index b9ef4667..4f7b7b0a 100644 --- a/pkg/code/data/chat/v2/id.go +++ b/pkg/code/data/chat/v2/id.go @@ -13,14 +13,14 @@ import ( type ChatId [32]byte -// GetChatIdFromProto gets a chat ID from the protobuf variant -func GetChatIdFromProto(proto *chatpb.ChatId) (ChatId, error) { - if err := proto.Validate(); err != nil { - return ChatId{}, errors.Wrap(err, "proto validation failed") +// GetChatIdFromBytes gets a chat ID from a byte buffer +func GetChatIdFromBytes(buffer []byte) (ChatId, error) { + if len(buffer) != 32 { + return ChatId{}, errors.New("chat id must be 32 bytes in length") } var typed ChatId - copy(typed[:], proto.Value) + copy(typed[:], buffer[:]) if err := typed.Validate(); err != nil { return ChatId{}, errors.Wrap(err, "invalid chat id") @@ -29,6 +29,15 @@ func GetChatIdFromProto(proto *chatpb.ChatId) (ChatId, error) { return typed, nil } +// GetChatIdFromProto gets a chat ID from the protobuf variant +func GetChatIdFromProto(proto *chatpb.ChatId) (ChatId, error) { + if err := proto.Validate(); err != nil { + return ChatId{}, errors.Wrap(err, "proto validation failed") + } + + return GetChatIdFromBytes(proto.Value) +} + // ToProto converts a chat ID to its protobuf variant func (c ChatId) ToProto() *chatpb.ChatId { return &chatpb.ChatId{Value: c[:]} @@ -52,14 +61,14 @@ func GenerateMemberId() MemberId { return MemberId(uuid.New()) } -// GetMemberIdFromProto gets a member ID from the protobuf variant -func GetMemberIdFromProto(proto *chatpb.ChatMemberId) (MemberId, error) { - if err := proto.Validate(); err != nil { - return MemberId{}, errors.Wrap(err, "proto validation failed") +// GetMemberIdFromBytes gets a member ID from a byte buffer +func GetMemberIdFromBytes(buffer []byte) (MemberId, error) { + if len(buffer) != 16 { + return MemberId{}, errors.New("member id must be 16 bytes in length") } var typed MemberId - copy(typed[:], proto.Value) + copy(typed[:], buffer[:]) if err := typed.Validate(); err != nil { return MemberId{}, errors.Wrap(err, "invalid member id") @@ -68,6 +77,15 @@ func GetMemberIdFromProto(proto *chatpb.ChatMemberId) (MemberId, error) { return typed, nil } +// GetMemberIdFromProto gets a member ID from the protobuf variant +func GetMemberIdFromProto(proto *chatpb.ChatMemberId) (MemberId, error) { + if err := proto.Validate(); err != nil { + return MemberId{}, errors.Wrap(err, "proto validation failed") + } + + return GetMemberIdFromBytes(proto.Value) +} + // ToProto converts a message ID to its protobuf variant func (m MemberId) ToProto() *chatpb.ChatMemberId { return &chatpb.ChatMemberId{Value: m[:]} @@ -123,14 +141,14 @@ func GenerateMessageIdAtTime(ts time.Time) MessageId { return MessageId(uuidBytes) } -// GetMessageIdFromProto gets a message ID from the protobuf variant -func GetMessageIdFromProto(proto *chatpb.ChatMessageId) (MessageId, error) { - if err := proto.Validate(); err != nil { - return MessageId{}, errors.Wrap(err, "proto validation failed") +// GetMessageIdFromBytes gets a message ID from a byte buffer +func GetMessageIdFromBytes(buffer []byte) (MessageId, error) { + if len(buffer) != 16 { + return MessageId{}, errors.New("message id must be 16 bytes in length") } var typed MessageId - copy(typed[:], proto.Value) + copy(typed[:], buffer[:]) if err := typed.Validate(); err != nil { return MessageId{}, errors.Wrap(err, "invalid message id") @@ -139,6 +157,15 @@ func GetMessageIdFromProto(proto *chatpb.ChatMessageId) (MessageId, error) { return typed, nil } +// GetMessageIdFromProto gets a message ID from the protobuf variant +func GetMessageIdFromProto(proto *chatpb.ChatMessageId) (MessageId, error) { + if err := proto.Validate(); err != nil { + return MessageId{}, errors.Wrap(err, "proto validation failed") + } + + return GetMessageIdFromBytes(proto.Value) +} + // ToProto converts a message ID to its protobuf variant func (m MessageId) ToProto() *chatpb.ChatMessageId { return &chatpb.ChatMessageId{Value: m[:]} diff --git a/pkg/code/data/chat/v2/memory/store.go b/pkg/code/data/chat/v2/memory/store.go index e3802aa0..c5feb913 100644 --- a/pkg/code/data/chat/v2/memory/store.go +++ b/pkg/code/data/chat/v2/memory/store.go @@ -3,8 +3,9 @@ package memory import ( "bytes" "context" - "errors" + "sort" "sync" + "time" chat "github.com/code-payments/code-server/pkg/code/data/chat/v2" "github.com/code-payments/code-server/pkg/database/query" @@ -18,9 +19,9 @@ type store struct { memberRecords []*chat.MemberRecord messageRecords []*chat.MessageRecord - lastChatId uint64 - lastMemberId uint64 - lastMessageId uint64 + lastChatId int64 + lastMemberId int64 + lastMessageId int64 } // New returns a new in memory chat.Store @@ -75,31 +76,91 @@ func (s *store) GetAllMessagesByChat(_ context.Context, chatId chat.ChatId, curs s.mu.Lock() defer s.mu.Unlock() - return nil, errors.New("not implemented") + items := s.findMessagesByChatId(chatId) + items, err := s.getMessageRecordPage(items, cursor, direction, limit) + if err != nil { + return nil, err + } + if len(items) == 0 { + return nil, chat.ErrMessageNotFound + } + + return cloneMessageRecords(items), nil } // PutChat creates a new chat func (s *store) PutChat(_ context.Context, record *chat.ChatRecord) error { + if err := record.Validate(); err != nil { + return err + } + s.mu.Lock() defer s.mu.Unlock() - return errors.New("not implemented") + s.lastChatId++ + + if item := s.findChat(record); item != nil { + return chat.ErrChatExists + } + + record.Id = s.lastChatId + if record.CreatedAt.IsZero() { + record.CreatedAt = time.Now() + } + + cloned := record.Clone() + s.chatRecords = append(s.chatRecords, &cloned) + + return nil } // PutMember creates a new chat member func (s *store) PutMember(_ context.Context, record *chat.MemberRecord) error { + if err := record.Validate(); err != nil { + return err + } + s.mu.Lock() defer s.mu.Unlock() - return errors.New("not implemented") + s.lastMemberId++ + + if item := s.findMember(record); item != nil { + return chat.ErrMemberExists + } + + record.Id = s.lastMemberId + if record.JoinedAt.IsZero() { + record.JoinedAt = time.Now() + } + + cloned := record.Clone() + s.memberRecords = append(s.memberRecords, &cloned) + + return nil } // PutMessage implements chat.Store.PutMessage func (s *store) PutMessage(_ context.Context, record *chat.MessageRecord) error { + if err := record.Validate(); err != nil { + return err + } + s.mu.Lock() defer s.mu.Unlock() - return errors.New("not implemented") + s.lastMessageId++ + + if item := s.findMessage(record); item != nil { + return chat.ErrMessageExsits + } + + record.Id = s.lastMessageId + + cloned := record.Clone() + s.messageRecords = append(s.messageRecords, &cloned) + + return nil } // AdvancePointer implements chat.Store.AdvancePointer @@ -170,6 +231,19 @@ func (s *store) SetSubscriptionState(_ context.Context, chatId chat.ChatId, memb return nil } +func (s *store) findChat(data *chat.ChatRecord) *chat.ChatRecord { + for _, item := range s.chatRecords { + if data.Id == item.Id { + return item + } + + if bytes.Equal(data.ChatId[:], item.ChatId[:]) { + return item + } + } + return nil +} + func (s *store) findChatById(chatId chat.ChatId) *chat.ChatRecord { for _, item := range s.chatRecords { if bytes.Equal(chatId[:], item.ChatId[:]) { @@ -179,6 +253,19 @@ func (s *store) findChatById(chatId chat.ChatId) *chat.ChatRecord { return nil } +func (s *store) findMember(data *chat.MemberRecord) *chat.MemberRecord { + for _, item := range s.memberRecords { + if data.Id == item.Id { + return item + } + + if bytes.Equal(data.ChatId[:], item.ChatId[:]) && bytes.Equal(data.MemberId[:], item.MemberId[:]) { + return item + } + } + return nil +} + func (s *store) findMemberById(chatId chat.ChatId, memberId chat.MemberId) *chat.MemberRecord { for _, item := range s.memberRecords { if bytes.Equal(chatId[:], item.ChatId[:]) && bytes.Equal(memberId[:], item.MemberId[:]) { @@ -188,6 +275,19 @@ func (s *store) findMemberById(chatId chat.ChatId, memberId chat.MemberId) *chat return nil } +func (s *store) findMessage(data *chat.MessageRecord) *chat.MessageRecord { + for _, item := range s.messageRecords { + if data.Id == item.Id { + return item + } + + if bytes.Equal(data.ChatId[:], item.ChatId[:]) && bytes.Equal(data.MessageId[:], item.MessageId[:]) { + return item + } + } + return nil +} + func (s *store) findMessageById(chatId chat.ChatId, messageId chat.MessageId) *chat.MessageRecord { for _, item := range s.messageRecords { if bytes.Equal(chatId[:], item.ChatId[:]) && bytes.Equal(messageId[:], item.MessageId[:]) { @@ -197,6 +297,58 @@ func (s *store) findMessageById(chatId chat.ChatId, messageId chat.MessageId) *c return nil } +func (s *store) findMessagesByChatId(chatId chat.ChatId) []*chat.MessageRecord { + var res []*chat.MessageRecord + for _, item := range s.messageRecords { + if bytes.Equal(chatId[:], item.ChatId[:]) { + res = append(res, item) + } + } + return res +} + +func (s *store) getMessageRecordPage(items []*chat.MessageRecord, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MessageRecord, error) { + if len(items) == 0 { + return nil, nil + } + + var messageIdCursor *chat.MessageId + if len(cursor) > 0 { + messageId, err := chat.GetMessageIdFromBytes(cursor) + if err != nil { + return nil, err + } + messageIdCursor = &messageId + } + + var res []*chat.MessageRecord + if messageIdCursor == nil { + res = items + } else { + for _, item := range items { + if item.MessageId.After(*messageIdCursor) && direction == query.Ascending { + res = append(res, item) + } + + if item.MessageId.Before(*messageIdCursor) && direction == query.Descending { + res = append(res, item) + } + } + } + + if direction == query.Ascending { + sort.Sort(chat.MessagesById(res)) + } else { + sort.Sort(sort.Reverse(chat.MessagesById(res))) + } + + if len(res) >= int(limit) { + return res[:limit], nil + } + + return res, nil +} + func (s *store) reset() { s.mu.Lock() defer s.mu.Unlock() @@ -209,3 +361,12 @@ func (s *store) reset() { s.lastMemberId = 0 s.lastMessageId = 0 } + +func cloneMessageRecords(items []*chat.MessageRecord) []*chat.MessageRecord { + res := make([]*chat.MessageRecord, len(items)) + for i, item := range items { + cloned := item.Clone() + res[i] = &cloned + } + return res +} diff --git a/pkg/code/data/chat/v2/model.go b/pkg/code/data/chat/v2/model.go index d68a76eb..47ce577c 100644 --- a/pkg/code/data/chat/v2/model.go +++ b/pkg/code/data/chat/v2/model.go @@ -97,6 +97,14 @@ type MessageRecord struct { // Note: No timestamp field, since it's encoded in MessageId } +type MessagesById []*MessageRecord + +func (a MessagesById) Len() int { return len(a) } +func (a MessagesById) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a MessagesById) Less(i, j int) bool { + return a[i].MessageId.Before(a[i].MessageId) +} + // GetChatIdFromProto gets a chat ID from the protobuf variant func GetPointerTypeFromProto(proto chatpb.Pointer_Kind) PointerType { switch proto { diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index e7234b3a..2312e966 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -8,8 +8,11 @@ import ( ) var ( + ErrChatExists = errors.New("chat already exists") ErrChatNotFound = errors.New("chat not found") + ErrMemberExists = errors.New("chat member already exists") ErrMemberNotFound = errors.New("chat member not found") + ErrMessageExsits = errors.New("chat message already exists") ErrMessageNotFound = errors.New("chat message not found") ErrInvalidPointerType = errors.New("invalid pointer type") ) From a2cda00623611e7142e36634414f8d5daa4fcc81 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 11 Jun 2024 11:14:13 -0400 Subject: [PATCH 24/40] Add missing result codes and update/comment on flush --- go.mod | 2 +- go.sum | 4 +- pkg/code/server/grpc/chat/v2/server.go | 56 ++++++++++++++++++++------ 3 files changed, 46 insertions(+), 16 deletions(-) diff --git a/go.mod b/go.mod index 186ecead..4d7d15ed 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.7-0.20240607133020-80fe3ddf808d + github.com/code-payments/code-protobuf-api v1.16.7-0.20240611151313-ca7587f92a73 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index 443a698d..2fe310ad 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240607133020-80fe3ddf808d h1:Xevf1deSLA4o+jMWpTD6LB0zqvsmRFlYhhexKGZPvpE= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240607133020-80fe3ddf808d/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240611151313-ca7587f92a73 h1:gdj/RvbLkcfxeWsrHJSu6Z8rkNtWvrIMZz/1WQlxVyg= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240611151313-ca7587f92a73/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index b3f815fe..5796e312 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -126,7 +126,9 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest switch err { case nil: case chat.ErrChatNotFound: - return nil, status.Error(codes.Unimplemented, "todo: missing result code") + return &chatpb.GetMessagesResponse{ + Result: chatpb.GetMessagesResponse_MESSAGE_NOT_FOUND, + }, nil default: log.WithError(err).Warn("failure getting chat record") return nil, status.Error(codes.Internal, "") @@ -137,7 +139,9 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") } else if !ownsChatMember { - return nil, status.Error(codes.Unimplemented, "todo: missing result code") + return &chatpb.GetMessagesResponse{ + Result: chatpb.GetMessagesResponse_DENIED, + }, nil } var limit uint64 @@ -175,6 +179,11 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest return nil, status.Error(codes.Internal, "") } + if len(protoChatMessages) == 0 { + return &chatpb.GetMessagesResponse{ + Result: chatpb.GetMessagesResponse_MESSAGE_NOT_FOUND, + }, nil + } return &chatpb.GetMessagesResponse{ Result: chatpb.GetMessagesResponse_OK, Messages: protoChatMessages, @@ -227,7 +236,11 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e switch err { case nil: case chat.ErrChatNotFound: - return status.Error(codes.Unimplemented, "todo: missing result code") + return streamer.Send(&chatpb.StreamChatEventsResponse{ + Type: &chatpb.StreamChatEventsResponse_Error{ + Error: &chatpb.ChatStreamEventError{Code: chatpb.ChatStreamEventError_CHAT_NOT_FOUND}, + }, + }) default: log.WithError(err).Warn("failure getting chat record") return status.Error(codes.Internal, "") @@ -238,7 +251,11 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e log.WithError(err).Warn("failure determing chat member ownership") return status.Error(codes.Internal, "") } else if !ownsChatMember { - return status.Error(codes.Unimplemented, "todo: missing result code") + return streamer.Send(&chatpb.StreamChatEventsResponse{ + Type: &chatpb.StreamChatEventsResponse_Error{ + Error: &chatpb.ChatStreamEventError{Code: chatpb.ChatStreamEventError_DENIED}, + }, + }) } streamKey := fmt.Sprintf("%s:%s", chatId.String(), memberId.String()) @@ -282,7 +299,8 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e sendPingCh := time.After(0) streamHealthCh := monitorChatEventStreamHealth(ctx, log, streamRef, streamer) - go s.flush(ctx, chatId, owner, stream) + // todo: We should also "flush" pointers for each chat member + go s.flushMessages(ctx, chatId, owner, stream) for { select { @@ -330,9 +348,9 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e } } -func (s *server) flush(ctx context.Context, chatId chat.ChatId, owner *common.Account, stream *chatEventStream) { +func (s *server) flushMessages(ctx context.Context, chatId chat.ChatId, owner *common.Account, stream *chatEventStream) { log := s.log.WithFields(logrus.Fields{ - "method": "flush", + "method": "flushMessages", "chat_id": chatId.String(), "owner_account": owner.PublicKey().ToBase58(), }) @@ -347,7 +365,9 @@ func (s *server) flush(ctx context.Context, chatId chat.ChatId, owner *common.Ac query.WithDirection(query.Descending), query.WithLimit(flushMessageCount), ) - if err != nil { + if err == chat.ErrMessageNotFound { + return + } else if err != nil { log.WithError(err).Warn("failure getting chat messages") return } @@ -429,7 +449,9 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") } else if !ownsChatMember { - return nil, status.Error(codes.Unimplemented, "todo: missing result code") + return &chatpb.SendMessageResponse{ + Result: chatpb.SendMessageResponse_DENIED, + }, nil } chatLock := s.chatLocks.Get(chatId[:]) @@ -551,7 +573,9 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR switch pointerType { case chat.PointerTypeDelivered, chat.PointerTypeRead: default: - return nil, status.Error(codes.Unimplemented, "todo: missing result code") + return &chatpb.AdvancePointerResponse{ + Result: chatpb.AdvancePointerResponse_INVALID_POINTER_TYPE, + }, nil } pointerValue, err := chat.GetMessageIdFromProto(req.Pointer.Value) @@ -584,7 +608,9 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") } else if !ownsChatMember { - return nil, status.Error(codes.Unimplemented, "todo: missing result code") + return &chatpb.AdvancePointerResponse{ + Result: chatpb.AdvancePointerResponse_DENIED, + }, nil } _, err = s.data.GetChatMessageByIdV2(ctx, chatId, pointerValue) @@ -669,7 +695,9 @@ func (s *server) SetMuteState(ctx context.Context, req *chatpb.SetMuteStateReque log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") } else if !isChatMember { - return nil, status.Error(codes.Unimplemented, "todo: missing result code") + return &chatpb.SetMuteStateResponse{ + Result: chatpb.SetMuteStateResponse_DENIED, + }, nil } err = s.data.SetChatMuteStateV2(ctx, chatId, memberId, req.IsMuted) @@ -732,7 +760,9 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") } else if !ownsChatMember { - return nil, status.Error(codes.Unimplemented, "todo: missing result code") + return &chatpb.SetSubscriptionStateResponse{ + Result: chatpb.SetSubscriptionStateResponse_DENIED, + }, nil } err = s.data.SetChatSubscriptionStateV2(ctx, chatId, memberId, req.IsSubscribed) From f2f99d6a128c9beb6063c96c47cefb0a142f0fce Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 11 Jun 2024 11:52:16 -0400 Subject: [PATCH 25/40] Flush pointers on chat event stream open --- pkg/code/data/chat/v2/memory/store.go | 34 +++++++++++++++++-- pkg/code/data/chat/v2/model.go | 14 ++++++++ pkg/code/data/chat/v2/store.go | 9 ++++-- pkg/code/data/internal.go | 6 +++- pkg/code/server/grpc/chat/v2/server.go | 45 +++++++++++++++++++++++++- 5 files changed, 101 insertions(+), 7 deletions(-) diff --git a/pkg/code/data/chat/v2/memory/store.go b/pkg/code/data/chat/v2/memory/store.go index c5feb913..ce1184b5 100644 --- a/pkg/code/data/chat/v2/memory/store.go +++ b/pkg/code/data/chat/v2/memory/store.go @@ -71,8 +71,17 @@ func (s *store) GetMessageById(_ context.Context, chatId chat.ChatId, messageId return &cloned, nil } -// GetAllMessagesByChat implements chat.Store.GetAllMessagesByChat -func (s *store) GetAllMessagesByChat(_ context.Context, chatId chat.ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MessageRecord, error) { +// GetAllMembersByChatId implements chat.Store.GetAllMembersByChatId +func (s *store) GetAllMembersByChatId(_ context.Context, chatId chat.ChatId) ([]*chat.MemberRecord, error) { + items := s.findMembersByChatId(chatId) + if len(items) == 0 { + return nil, chat.ErrMemberNotFound + } + return cloneMemberRecords(items), nil +} + +// GetAllMessagesByChatId implements chat.Store.GetAllMessagesByChatId +func (s *store) GetAllMessagesByChatId(_ context.Context, chatId chat.ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MessageRecord, error) { s.mu.Lock() defer s.mu.Unlock() @@ -81,10 +90,10 @@ func (s *store) GetAllMessagesByChat(_ context.Context, chatId chat.ChatId, curs if err != nil { return nil, err } + if len(items) == 0 { return nil, chat.ErrMessageNotFound } - return cloneMessageRecords(items), nil } @@ -275,6 +284,16 @@ func (s *store) findMemberById(chatId chat.ChatId, memberId chat.MemberId) *chat return nil } +func (s *store) findMembersByChatId(chatId chat.ChatId) []*chat.MemberRecord { + var res []*chat.MemberRecord + for _, item := range s.memberRecords { + if bytes.Equal(chatId[:], item.ChatId[:]) { + res = append(res, item) + } + } + return res +} + func (s *store) findMessage(data *chat.MessageRecord) *chat.MessageRecord { for _, item := range s.messageRecords { if data.Id == item.Id { @@ -362,6 +381,15 @@ func (s *store) reset() { s.lastMessageId = 0 } +func cloneMemberRecords(items []*chat.MemberRecord) []*chat.MemberRecord { + res := make([]*chat.MemberRecord, len(items)) + for i, item := range items { + cloned := item.Clone() + res[i] = &cloned + } + return res +} + func cloneMessageRecords(items []*chat.MessageRecord) []*chat.MessageRecord { res := make([]*chat.MessageRecord, len(items)) for i, item := range items { diff --git a/pkg/code/data/chat/v2/model.go b/pkg/code/data/chat/v2/model.go index 47ce577c..39df38a8 100644 --- a/pkg/code/data/chat/v2/model.go +++ b/pkg/code/data/chat/v2/model.go @@ -119,6 +119,20 @@ func GetPointerTypeFromProto(proto chatpb.Pointer_Kind) PointerType { } } +// ToProto returns the proto representation of the pointer type +func (p PointerType) ToProto() chatpb.Pointer_Kind { + switch p { + case PointerTypeSent: + return chatpb.Pointer_SENT + case PointerTypeDelivered: + return chatpb.Pointer_DELIVERED + case PointerTypeRead: + return chatpb.Pointer_READ + default: + return chatpb.Pointer_UNKNOWN + } +} + // String returns the string representation of the pointer type func (p PointerType) String() string { switch p { diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index 2312e966..7ecc10fc 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -28,10 +28,15 @@ type Store interface { // GetMessageById gets a chat message by the chat and message IDs GetMessageById(ctx context.Context, chatId ChatId, messageId MessageId) (*MessageRecord, error) - // GetAllMessagesByChat gets all messages for a given chat + // GetAllMembersByChatId gets all members for a given chat + // + // todo: Add paging when we introduce group chats + GetAllMembersByChatId(ctx context.Context, chatId ChatId) ([]*MemberRecord, error) + + // GetAllMessagesByChatId gets all messages for a given chat // // Note: Cursor is a message ID - GetAllMessagesByChat(ctx context.Context, chatId ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*MessageRecord, error) + GetAllMessagesByChatId(ctx context.Context, chatId ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*MessageRecord, error) // PutChat creates a new chat PutChat(ctx context.Context, record *ChatRecord) error diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 68cce410..ff0220f5 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -399,6 +399,7 @@ type DatabaseData interface { GetChatByIdV2(ctx context.Context, chatId chat_v2.ChatId) (*chat_v2.ChatRecord, error) GetChatMemberByIdV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId) (*chat_v2.MemberRecord, error) GetChatMessageByIdV2(ctx context.Context, chatId chat_v2.ChatId, messageId chat_v2.MessageId) (*chat_v2.MessageRecord, error) + GetAllChatMembersV2(ctx context.Context, chatId chat_v2.ChatId) ([]*chat_v2.MemberRecord, error) GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) PutChatV2(ctx context.Context, record *chat_v2.ChatRecord) error PutChatMemberV2(ctx context.Context, record *chat_v2.MemberRecord) error @@ -1472,12 +1473,15 @@ func (dp *DatabaseProvider) GetChatMemberByIdV2(ctx context.Context, chatId chat func (dp *DatabaseProvider) GetChatMessageByIdV2(ctx context.Context, chatId chat_v2.ChatId, messageId chat_v2.MessageId) (*chat_v2.MessageRecord, error) { return dp.chatv2.GetMessageById(ctx, chatId, messageId) } +func (dp *DatabaseProvider) GetAllChatMembersV2(ctx context.Context, chatId chat_v2.ChatId) ([]*chat_v2.MemberRecord, error) { + return dp.chatv2.GetAllMembersByChatId(ctx, chatId) +} func (dp *DatabaseProvider) GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) { req, err := query.DefaultPaginationHandler(opts...) if err != nil { return nil, err } - return dp.chatv2.GetAllMessagesByChat(ctx, chatId, req.Cursor, req.SortBy, req.Limit) + return dp.chatv2.GetAllMessagesByChatId(ctx, chatId, req.Cursor, req.SortBy, req.Limit) } func (dp *DatabaseProvider) PutChatV2(ctx context.Context, record *chat_v2.ChatRecord) error { return dp.chatv2.PutChat(ctx, record) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 5796e312..7e33bd96 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -299,8 +299,8 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e sendPingCh := time.After(0) streamHealthCh := monitorChatEventStreamHealth(ctx, log, streamRef, streamer) - // todo: We should also "flush" pointers for each chat member go s.flushMessages(ctx, chatId, owner, stream) + go s.flushPointers(ctx, chatId, stream) for { select { @@ -385,6 +385,49 @@ func (s *server) flushMessages(ctx context.Context, chatId chat.ChatId, owner *c } } +func (s *server) flushPointers(ctx context.Context, chatId chat.ChatId, stream *chatEventStream) { + log := s.log.WithFields(logrus.Fields{ + "method": "flushPointers", + "chat_id": chatId.String(), + }) + + memberRecords, err := s.data.GetAllChatMembersV2(ctx, chatId) + if err == chat.ErrMemberNotFound { + return + } else if err != nil { + log.WithError(err).Warn("failure getting chat members") + return + } + + for _, memberRecord := range memberRecords { + for _, optionalPointer := range []struct { + kind chat.PointerType + value *chat.MessageId + }{ + {chat.PointerTypeDelivered, memberRecord.DeliveryPointer}, + {chat.PointerTypeRead, memberRecord.ReadPointer}, + } { + if optionalPointer.value == nil { + continue + } + + event := &chatpb.ChatStreamEvent{ + Type: &chatpb.ChatStreamEvent_Pointer{ + Pointer: &chatpb.Pointer{ + Kind: optionalPointer.kind.ToProto(), + Value: optionalPointer.value.ToProto(), + MemberId: memberRecord.MemberId.ToProto(), + }, + }, + } + if err := stream.notify(event, streamNotifyTimeout); err != nil { + log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) + return + } + } + } +} + func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest) (*chatpb.SendMessageResponse, error) { log := s.log.WithField("method", "SendMessage") log = client.InjectLoggingMetadata(ctx, log) From 1ed48623fb71192fdf6870dd512e4a215cda3870 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 11 Jun 2024 12:22:56 -0400 Subject: [PATCH 26/40] Address todos relating to copying pointer values in chat store --- pkg/code/data/chat/v2/id.go | 21 ++++++++ pkg/code/data/chat/v2/memory/store.go | 6 ++- pkg/code/data/chat/v2/model.go | 77 ++++++++++++++++++++------- pkg/pointer/pointer.go | 30 +++++++++++ 4 files changed, 112 insertions(+), 22 deletions(-) diff --git a/pkg/code/data/chat/v2/id.go b/pkg/code/data/chat/v2/id.go index 4f7b7b0a..26341e6e 100644 --- a/pkg/code/data/chat/v2/id.go +++ b/pkg/code/data/chat/v2/id.go @@ -48,6 +48,13 @@ func (c ChatId) Validate() error { return nil } +// Clone clones a chat ID +func (c ChatId) Clone() ChatId { + var cloned ChatId + copy(cloned[:], c[:]) + return cloned +} + // String returns the string representation of a ChatId func (c ChatId) String() string { return hex.EncodeToString(c[:]) @@ -102,6 +109,13 @@ func (m MemberId) Validate() error { return nil } +// Clone clones a chat member ID +func (m MemberId) Clone() MemberId { + var cloned MemberId + copy(cloned[:], m[:]) + return cloned +} + // String returns the string representation of a MemberId func (m MemberId) String() string { return uuid.UUID(m).String() @@ -218,6 +232,13 @@ func (m MessageId) Validate() error { return nil } +// Clone clones a chat message ID +func (m MessageId) Clone() MessageId { + var cloned MessageId + copy(cloned[:], m[:]) + return cloned +} + // String returns the string representation of a MessageId func (m MessageId) String() string { return uuid.UUID(m).String() diff --git a/pkg/code/data/chat/v2/memory/store.go b/pkg/code/data/chat/v2/memory/store.go index ce1184b5..2c051fad 100644 --- a/pkg/code/data/chat/v2/memory/store.go +++ b/pkg/code/data/chat/v2/memory/store.go @@ -202,9 +202,11 @@ func (s *store) AdvancePointer(_ context.Context, chatId chat.ChatId, memberId c switch pointerType { case chat.PointerTypeDelivered: - item.DeliveryPointer = &pointer // todo: pointer copy safety + cloned := pointer.Clone() + item.DeliveryPointer = &cloned case chat.PointerTypeRead: - item.ReadPointer = &pointer // todo: pointer copy safety + cloned := pointer.Clone() + item.ReadPointer = &cloned } return nil diff --git a/pkg/code/data/chat/v2/model.go b/pkg/code/data/chat/v2/model.go index 39df38a8..f30b78c1 100644 --- a/pkg/code/data/chat/v2/model.go +++ b/pkg/code/data/chat/v2/model.go @@ -11,7 +11,7 @@ import ( chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v2" ) -type ChatType int +type ChatType uint8 const ( ChatTypeUnknown ChatType = iota @@ -20,7 +20,7 @@ const ( // ChatTypeGroup ) -type ReferenceType int +type ReferenceType uint8 const ( ReferenceTypeUnknown ReferenceType = iota @@ -28,7 +28,7 @@ const ( ReferenceTypeSignature ) -type PointerType int +type PointerType uint8 const ( PointerTypeUnknown PointerType = iota @@ -37,7 +37,7 @@ const ( PointerTypeRead ) -type Platform int +type Platform uint8 const ( PlatformUnknown Platform = iota @@ -256,6 +256,18 @@ func (r *MemberRecord) Validate() error { // Clone clones a member record func (r *MemberRecord) Clone() MemberRecord { + var deliveryPointerCopy *MessageId + if r.DeliveryPointer != nil { + cloned := r.DeliveryPointer.Clone() + deliveryPointerCopy = &cloned + } + + var readPointerCopy *MessageId + if r.ReadPointer != nil { + cloned := r.ReadPointer.Clone() + readPointerCopy = &cloned + } + return MemberRecord{ Id: r.Id, ChatId: r.ChatId, @@ -264,8 +276,8 @@ func (r *MemberRecord) Clone() MemberRecord { Platform: r.Platform, PlatformId: r.PlatformId, - DeliveryPointer: r.DeliveryPointer, // todo: pointer copy safety - ReadPointer: r.ReadPointer, // todo: pointer copy safety + DeliveryPointer: deliveryPointerCopy, + ReadPointer: readPointerCopy, IsMuted: r.IsMuted, IsUnsubscribed: r.IsUnsubscribed, @@ -283,8 +295,14 @@ func (r *MemberRecord) CopyTo(dst *MemberRecord) { dst.Platform = r.Platform dst.PlatformId = r.PlatformId - dst.DeliveryPointer = r.DeliveryPointer // todo: pointer copy safety - dst.ReadPointer = r.ReadPointer // todo: pointer copy safety + if r.DeliveryPointer != nil { + cloned := r.DeliveryPointer.Clone() + dst.DeliveryPointer = &cloned + } + if r.ReadPointer != nil { + cloned := r.ReadPointer.Clone() + dst.ReadPointer = &cloned + } dst.IsMuted = r.IsMuted dst.IsUnsubscribed = r.IsUnsubscribed @@ -350,21 +368,34 @@ func (r *MessageRecord) Validate() error { // Clone clones a message record func (r *MessageRecord) Clone() MessageRecord { + var senderCopy *MemberId + if r.Sender != nil { + cloned := r.Sender.Clone() + senderCopy = &cloned + } + + dataCopy := make([]byte, len(r.Data)) + copy(dataCopy, r.Data) + + var referenceTypeCopy *ReferenceType + if r.ReferenceType != nil { + cloned := *r.ReferenceType + referenceTypeCopy = &cloned + } + return MessageRecord{ Id: r.Id, ChatId: r.ChatId, MessageId: r.MessageId, - Sender: r.Sender, // todo: pointer copy safety + Sender: senderCopy, - Data: r.Data, // todo: pointer copy safety + Data: dataCopy, - ReferenceType: r.ReferenceType, // todo: pointer copy safety - Reference: r.Reference, // todo: pointer copy safety + ReferenceType: referenceTypeCopy, + Reference: pointer.StringCopy(r.Reference), IsSilent: r.IsSilent, - - // todo: finish implementing me } } @@ -374,16 +405,22 @@ func (r *MessageRecord) CopyTo(dst *MessageRecord) { dst.ChatId = r.ChatId dst.MessageId = r.MessageId - dst.Sender = r.Sender // todo: pointer copy safety + if r.Sender != nil { + cloned := r.Sender.Clone() + dst.Sender = &cloned + } - dst.Data = r.Data // todo: pointer copy safety + dataCopy := make([]byte, len(r.Data)) + copy(dataCopy, r.Data) + dst.Data = dataCopy - dst.ReferenceType = r.ReferenceType // todo: pointer copy safety - dst.Reference = r.Reference // todo: pointer copy safety + if r.ReferenceType != nil { + cloned := *r.ReferenceType + dst.ReferenceType = &cloned + } + dst.Reference = pointer.StringCopy(r.Reference) dst.IsSilent = r.IsSilent - - // todo: finish implementing me } // GetTimestamp gets the timestamp for a message record diff --git a/pkg/pointer/pointer.go b/pkg/pointer/pointer.go index a3f8da02..a353d347 100644 --- a/pkg/pointer/pointer.go +++ b/pkg/pointer/pointer.go @@ -32,6 +32,36 @@ func StringCopy(value *string) *string { return String(*value) } +// Uint8 returns a pointer to the provided uint8 value +func Uint8(value uint8) *uint8 { + return &value +} + +// Uint8OrDefault returns the pointer if not nil, otherwise the default value +func Uint8OrDefault(value *uint8, defaultValue uint8) *uint8 { + if value != nil { + return value + } + return &defaultValue +} + +// Uint8IfValid returns a pointer to the value if it's valid, otherwise nil +func Uint8IfValid(valid bool, value uint8) *uint8 { + if valid { + return &value + } + return nil +} + +// Uint8Copy returns a pointer that's a copy of the provided value +func Uint8Copy(value *uint8) *uint8 { + if value == nil { + return nil + } + + return Uint8(*value) +} + // Uint64 returns a pointer to the provided uint64 value func Uint64(value uint64) *uint64 { return &value From 0b8ec7b1b8fff49497ed77b22049de52aca5c9e7 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 11 Jun 2024 14:34:59 -0400 Subject: [PATCH 27/40] Fix result codes in GetMessages --- pkg/code/server/grpc/chat/v2/server.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 7e33bd96..775d848f 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -127,7 +127,7 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest case nil: case chat.ErrChatNotFound: return &chatpb.GetMessagesResponse{ - Result: chatpb.GetMessagesResponse_MESSAGE_NOT_FOUND, + Result: chatpb.GetMessagesResponse_CHAT_NOT_FOUND, }, nil default: log.WithError(err).Warn("failure getting chat record") @@ -174,7 +174,11 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest query.WithDirection(direction), query.WithLimit(limit), ) - if err != nil { + if err == chat.ErrMessageNotFound { + return &chatpb.GetMessagesResponse{ + Result: chatpb.GetMessagesResponse_MESSAGE_NOT_FOUND, + }, nil + } else if err != nil { log.WithError(err).Warn("failure getting chat messages") return nil, status.Error(codes.Internal, "") } From c7a4b53fcac51754418b52290560bac4de16d0a4 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 11 Jun 2024 15:24:28 -0400 Subject: [PATCH 28/40] Setup a temporary mock chat for testing --- pkg/code/data/chat/v2/id.go | 30 +++++++++++++++++++++ pkg/code/server/grpc/chat/v2/server.go | 36 ++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/pkg/code/data/chat/v2/id.go b/pkg/code/data/chat/v2/id.go index 26341e6e..de912e48 100644 --- a/pkg/code/data/chat/v2/id.go +++ b/pkg/code/data/chat/v2/id.go @@ -29,6 +29,16 @@ func GetChatIdFromBytes(buffer []byte) (ChatId, error) { return typed, nil } +// GetChatIdFromBytes gets a chat ID from the string representation +func GetChatIdFromString(value string) (ChatId, error) { + decoded, err := hex.DecodeString(value) + if err != nil { + return ChatId{}, errors.Wrap(err, "value is not a hexadecimal string") + } + + return GetChatIdFromBytes(decoded) +} + // GetChatIdFromProto gets a chat ID from the protobuf variant func GetChatIdFromProto(proto *chatpb.ChatId) (ChatId, error) { if err := proto.Validate(); err != nil { @@ -84,6 +94,16 @@ func GetMemberIdFromBytes(buffer []byte) (MemberId, error) { return typed, nil } +// GetMemberIdFromString gets a chat member ID from the string representation +func GetMemberIdFromString(value string) (MemberId, error) { + decoded, err := uuid.Parse(value) + if err != nil { + return MemberId{}, errors.Wrap(err, "value is not a uuid string") + } + + return GetMemberIdFromBytes(decoded[:]) +} + // GetMemberIdFromProto gets a member ID from the protobuf variant func GetMemberIdFromProto(proto *chatpb.ChatMemberId) (MemberId, error) { if err := proto.Validate(); err != nil { @@ -171,6 +191,16 @@ func GetMessageIdFromBytes(buffer []byte) (MessageId, error) { return typed, nil } +// GetMessageIdFromString gets a chat message ID from the string representation +func GetMessageIdFromString(value string) (MessageId, error) { + decoded, err := uuid.Parse(value) + if err != nil { + return MessageId{}, errors.Wrap(err, "value is not a uuid string") + } + + return GetMessageIdFromBytes(decoded[:]) +} + // GetMessageIdFromProto gets a message ID from the protobuf variant func GetMessageIdFromProto(proto *chatpb.ChatMessageId) (MessageId, error) { if err := proto.Validate(); err != nil { diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 775d848f..a10f2ef9 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -68,9 +68,45 @@ func NewChatServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier go s.asyncChatEventStreamNotifier(i, channel) } + // todo: Remove when testing is complete + s.setupMockChat() + return s } +func (s *server) setupMockChat() { + ctx := context.Background() + + chatId, _ := chat.GetChatIdFromString("c355fcec8c521e7937d45283d83bbfc63a0c688004f2386a535fc817218f917b") + chatRecord := &chat.ChatRecord{ + ChatId: chatId, + ChatType: chat.ChatTypeTwoWay, + IsVerified: true, + CreatedAt: time.Now(), + } + s.data.PutChatV2(ctx, chatRecord) + + memberId1, _ := chat.GetMemberIdFromString("034dda45-b4c2-45db-b1da-181298898a16") + memberRecord1 := &chat.MemberRecord{ + ChatId: chatId, + MemberId: memberId1, + Platform: chat.PlatformCode, + PlatformId: "8bw4gaRQk91w7vtgTN4E12GnKecY2y6CjPai7WUvWBQ8", + JoinedAt: time.Now(), + } + s.data.PutChatMemberV2(ctx, memberRecord1) + + memberId2, _ := chat.GetMemberIdFromString("a9d27058-f2d8-4034-bf52-b20c09a670de") + memberRecord2 := &chat.MemberRecord{ + ChatId: chatId, + MemberId: memberId2, + Platform: chat.PlatformCode, + PlatformId: "EDknQfoUnj73L56vKtEc6Qqw5VoHaF32eHYdz3V4y27M", + JoinedAt: time.Now(), + } + s.data.PutChatMemberV2(ctx, memberRecord2) +} + func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*chatpb.GetChatsResponse, error) { log := s.log.WithField("method", "GetChats") log = client.InjectLoggingMetadata(ctx, log) From f2135f07236ce69e0a7982a1249094c8c0580b93 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 12 Jun 2024 09:47:28 -0400 Subject: [PATCH 29/40] Fix message ID sorting --- pkg/code/data/chat/v2/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/code/data/chat/v2/model.go b/pkg/code/data/chat/v2/model.go index f30b78c1..6d5124b0 100644 --- a/pkg/code/data/chat/v2/model.go +++ b/pkg/code/data/chat/v2/model.go @@ -102,7 +102,7 @@ type MessagesById []*MessageRecord func (a MessagesById) Len() int { return len(a) } func (a MessagesById) Swap(i, j int) { a[i], a[j] = a[j], a[i] } func (a MessagesById) Less(i, j int) bool { - return a[i].MessageId.Before(a[i].MessageId) + return a[i].MessageId.Before(a[j].MessageId) } // GetChatIdFromProto gets a chat ID from the protobuf variant From b0f183617d80d4492d347027e0c379bc8f4d3b55 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 12 Jun 2024 10:22:22 -0400 Subject: [PATCH 30/40] flushMessages doesn't need a cursor value for the DB query --- pkg/code/server/grpc/chat/v2/server.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index a10f2ef9..00f7749d 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -395,13 +395,11 @@ func (s *server) flushMessages(ctx context.Context, chatId chat.ChatId, owner *c "owner_account": owner.PublicKey().ToBase58(), }) - cursorValue := chat.GenerateMessageIdAtTime(time.Now().Add(2 * time.Second)) - protoChatMessages, err := s.getProtoChatMessages( ctx, chatId, owner, - query.WithCursor(cursorValue[:]), + query.WithCursor(query.EmptyCursor), query.WithDirection(query.Descending), query.WithLimit(flushMessageCount), ) From b48020ff84e6b9297dca4ff6e7f7176326ae14dd Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 13 Jun 2024 10:40:30 -0400 Subject: [PATCH 31/40] Implement GetChats RPC limited to anonymous chat membership --- go.mod | 2 +- go.sum | 4 +- pkg/code/data/chat/v2/memory/store.go | 101 ++++++++++++++++- pkg/code/data/chat/v2/model.go | 76 ++++++++++++- pkg/code/data/chat/v2/store.go | 7 ++ pkg/code/data/internal.go | 12 ++ pkg/code/server/grpc/chat/v2/server.go | 148 ++++++++++++++++++++++++- 7 files changed, 339 insertions(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 4d7d15ed..e5945d8a 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.7-0.20240611151313-ca7587f92a73 + github.com/code-payments/code-protobuf-api v1.16.7-0.20240613143759-2e4fbcd11dcb github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index 2fe310ad..1c691884 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240611151313-ca7587f92a73 h1:gdj/RvbLkcfxeWsrHJSu6Z8rkNtWvrIMZz/1WQlxVyg= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240611151313-ca7587f92a73/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240613143759-2e4fbcd11dcb h1:cSgl4rZkQqQ1cDgJFGxyRZFoHljN2Of73znDpqKXftY= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240613143759-2e4fbcd11dcb/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/pkg/code/data/chat/v2/memory/store.go b/pkg/code/data/chat/v2/memory/store.go index 2c051fad..97222fab 100644 --- a/pkg/code/data/chat/v2/memory/store.go +++ b/pkg/code/data/chat/v2/memory/store.go @@ -80,6 +80,34 @@ func (s *store) GetAllMembersByChatId(_ context.Context, chatId chat.ChatId) ([] return cloneMemberRecords(items), nil } +// GetAllMembersByPlatformId implements chat.store.GetAllMembersByPlatformId +func (s *store) GetAllMembersByPlatformId(_ context.Context, platform chat.Platform, platformId string, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MemberRecord, error) { + s.mu.Lock() + defer s.mu.Unlock() + + items := s.findMembersByPlatformId(platform, platformId) + items, err := s.getMemberRecordPage(items, cursor, direction, limit) + if err != nil { + return nil, err + } + + if len(items) == 0 { + return nil, chat.ErrMemberNotFound + } + return cloneMemberRecords(items), nil +} + +// GetUnreadCount implements chat.store.GetUnreadCount +func (s *store) GetUnreadCount(_ context.Context, chatId chat.ChatId, readPointer chat.MessageId) (uint32, error) { + s.mu.Lock() + defer s.mu.Unlock() + + items := s.findMessagesByChatId(chatId) + items = s.filterMessagesAfter(items, readPointer) + items = s.filterNotifiedMessages(items) + return uint32(len(items)), nil +} + // GetAllMessagesByChatId implements chat.Store.GetAllMessagesByChatId func (s *store) GetAllMessagesByChatId(_ context.Context, chatId chat.ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MessageRecord, error) { s.mu.Lock() @@ -296,6 +324,55 @@ func (s *store) findMembersByChatId(chatId chat.ChatId) []*chat.MemberRecord { return res } +func (s *store) findMembersByPlatformId(platform chat.Platform, platformId string) []*chat.MemberRecord { + var res []*chat.MemberRecord + for _, item := range s.memberRecords { + if platform == item.Platform && platformId == item.PlatformId { + res = append(res, item) + } + } + return res +} + +func (s *store) getMemberRecordPage(items []*chat.MemberRecord, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MemberRecord, error) { + if len(items) == 0 { + return nil, nil + } + + var memberIdCursor *uint64 + if len(cursor) > 0 { + cursorValue := query.FromCursor(cursor) + memberIdCursor = &cursorValue + } + + var res []*chat.MemberRecord + if memberIdCursor == nil { + res = items + } else { + for _, item := range items { + if item.Id > int64(*memberIdCursor) && direction == query.Ascending { + res = append(res, item) + } + + if item.Id < int64(*memberIdCursor) && direction == query.Descending { + res = append(res, item) + } + } + } + + if direction == query.Ascending { + sort.Sort(chat.MembersById(res)) + } else { + sort.Sort(sort.Reverse(chat.MembersById(res))) + } + + if len(res) >= int(limit) { + return res[:limit], nil + } + + return res, nil +} + func (s *store) findMessage(data *chat.MessageRecord) *chat.MessageRecord { for _, item := range s.messageRecords { if data.Id == item.Id { @@ -328,6 +405,26 @@ func (s *store) findMessagesByChatId(chatId chat.ChatId) []*chat.MessageRecord { return res } +func (s *store) filterMessagesAfter(items []*chat.MessageRecord, pointer chat.MessageId) []*chat.MessageRecord { + var res []*chat.MessageRecord + for _, item := range items { + if item.MessageId.After(pointer) { + res = append(res, item) + } + } + return res +} + +func (s *store) filterNotifiedMessages(items []*chat.MessageRecord) []*chat.MessageRecord { + var res []*chat.MessageRecord + for _, item := range items { + if !item.IsSilent { + res = append(res, item) + } + } + return res +} + func (s *store) getMessageRecordPage(items []*chat.MessageRecord, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MessageRecord, error) { if len(items) == 0 { return nil, nil @@ -358,9 +455,9 @@ func (s *store) getMessageRecordPage(items []*chat.MessageRecord, cursor query.C } if direction == query.Ascending { - sort.Sort(chat.MessagesById(res)) + sort.Sort(chat.MessagesByMessageId(res)) } else { - sort.Sort(sort.Reverse(chat.MessagesById(res))) + sort.Sort(sort.Reverse(chat.MessagesByMessageId(res))) } if len(res) >= int(limit) { diff --git a/pkg/code/data/chat/v2/model.go b/pkg/code/data/chat/v2/model.go index 6d5124b0..9a7108f3 100644 --- a/pkg/code/data/chat/v2/model.go +++ b/pkg/code/data/chat/v2/model.go @@ -97,15 +97,59 @@ type MessageRecord struct { // Note: No timestamp field, since it's encoded in MessageId } -type MessagesById []*MessageRecord +type MembersById []*MemberRecord -func (a MessagesById) Len() int { return len(a) } -func (a MessagesById) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a MessagesById) Less(i, j int) bool { +func (a MembersById) Len() int { return len(a) } +func (a MembersById) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a MembersById) Less(i, j int) bool { + return a[i].Id < a[j].Id +} + +type MessagesByMessageId []*MessageRecord + +func (a MessagesByMessageId) Len() int { return len(a) } +func (a MessagesByMessageId) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a MessagesByMessageId) Less(i, j int) bool { return a[i].MessageId.Before(a[j].MessageId) } -// GetChatIdFromProto gets a chat ID from the protobuf variant +// GetChatTypeFromProto gets a chat type from the protobuf variant +func GetChatTypeFromProto(proto chatpb.ChatMetadata_Kind) ChatType { + switch proto { + case chatpb.ChatMetadata_NOTIFICATION: + return ChatTypeNotification + case chatpb.ChatMetadata_TWO_WAY: + return ChatTypeTwoWay + default: + return ChatTypeUnknown + } +} + +// ToProto returns the proto representation of the chat type +func (c ChatType) ToProto() chatpb.ChatMetadata_Kind { + switch c { + case ChatTypeNotification: + return chatpb.ChatMetadata_NOTIFICATION + case ChatTypeTwoWay: + return chatpb.ChatMetadata_TWO_WAY + default: + return chatpb.ChatMetadata_UNKNOWN + } +} + +// String returns the string representation of the chat type +func (c ChatType) String() string { + switch c { + case ChatTypeNotification: + return "notification" + case ChatTypeTwoWay: + return "two-way" + default: + return "unknown" + } +} + +// GetPointerTypeFromProto gets a chat ID from the protobuf variant func GetPointerTypeFromProto(proto chatpb.Pointer_Kind) PointerType { switch proto { case chatpb.Pointer_SENT: @@ -147,6 +191,28 @@ func (p PointerType) String() string { } } +// ToProto returns the proto representation of the platform +func (p Platform) ToProto() chatpb.ChatMemberIdentity_Platform { + switch p { + case PlatformTwitter: + return chatpb.ChatMemberIdentity_TWITTER + default: + return chatpb.ChatMemberIdentity_UNKNOWN + } +} + +// String returns the string representation of the platform +func (p Platform) String() string { + switch p { + case PlatformCode: + return "code" + case PlatformTwitter: + return "twitter" + default: + return "unknown" + } +} + // Validate validates a chat Record func (r *ChatRecord) Validate() error { if err := r.ChatId.Validate(); err != nil { diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index 7ecc10fc..5a0e9f20 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -33,11 +33,18 @@ type Store interface { // todo: Add paging when we introduce group chats GetAllMembersByChatId(ctx context.Context, chatId ChatId) ([]*MemberRecord, error) + // GetAllMembersByPlatformId gets all members for a given platform user across + // all chats + GetAllMembersByPlatformId(ctx context.Context, platform Platform, platformId string, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*MemberRecord, error) + // GetAllMessagesByChatId gets all messages for a given chat // // Note: Cursor is a message ID GetAllMessagesByChatId(ctx context.Context, chatId ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*MessageRecord, error) + // GetUnreadCount gets the unread message count for a chat ID at a read pointer + GetUnreadCount(ctx context.Context, chatId ChatId, readPointer MessageId) (uint32, error) + // PutChat creates a new chat PutChat(ctx context.Context, record *ChatRecord) error diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index ff0220f5..68ab5816 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -400,7 +400,9 @@ type DatabaseData interface { GetChatMemberByIdV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId) (*chat_v2.MemberRecord, error) GetChatMessageByIdV2(ctx context.Context, chatId chat_v2.ChatId, messageId chat_v2.MessageId) (*chat_v2.MessageRecord, error) GetAllChatMembersV2(ctx context.Context, chatId chat_v2.ChatId) ([]*chat_v2.MemberRecord, error) + GetPlatformUserChatMembershipV2(ctx context.Context, platform chat_v2.Platform, platformId string, opts ...query.Option) ([]*chat_v2.MemberRecord, error) GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) + GetChatUnreadCountV2(ctx context.Context, chatId chat_v2.ChatId, readPointer chat_v2.MessageId) (uint32, error) PutChatV2(ctx context.Context, record *chat_v2.ChatRecord) error PutChatMemberV2(ctx context.Context, record *chat_v2.MemberRecord) error PutChatMessageV2(ctx context.Context, record *chat_v2.MessageRecord) error @@ -1476,6 +1478,13 @@ func (dp *DatabaseProvider) GetChatMessageByIdV2(ctx context.Context, chatId cha func (dp *DatabaseProvider) GetAllChatMembersV2(ctx context.Context, chatId chat_v2.ChatId) ([]*chat_v2.MemberRecord, error) { return dp.chatv2.GetAllMembersByChatId(ctx, chatId) } +func (dp *DatabaseProvider) GetPlatformUserChatMembershipV2(ctx context.Context, platform chat_v2.Platform, platformId string, opts ...query.Option) ([]*chat_v2.MemberRecord, error) { + req, err := query.DefaultPaginationHandler(opts...) + if err != nil { + return nil, err + } + return dp.chatv2.GetAllMembersByPlatformId(ctx, platform, platformId, req.Cursor, req.SortBy, req.Limit) +} func (dp *DatabaseProvider) GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) { req, err := query.DefaultPaginationHandler(opts...) if err != nil { @@ -1483,6 +1492,9 @@ func (dp *DatabaseProvider) GetAllChatMessagesV2(ctx context.Context, chatId cha } return dp.chatv2.GetAllMessagesByChatId(ctx, chatId, req.Cursor, req.SortBy, req.Limit) } +func (dp *DatabaseProvider) GetChatUnreadCountV2(ctx context.Context, chatId chat_v2.ChatId, readPointer chat_v2.MessageId) (uint32, error) { + return dp.chatv2.GetUnreadCount(ctx, chatId, readPointer) +} func (dp *DatabaseProvider) PutChatV2(ctx context.Context, record *chat_v2.ChatRecord) error { return dp.chatv2.PutChat(ctx, record) } diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 00f7749d..d0448565 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -1,8 +1,10 @@ package chat_v2 import ( + "bytes" "context" "fmt" + "math" "sync" "time" @@ -31,6 +33,7 @@ import ( ) const ( + maxGetChatsPageSize = 100 maxGetMessagesPageSize = 100 flushMessageCount = 100 ) @@ -107,6 +110,7 @@ func (s *server) setupMockChat() { s.data.PutChatMemberV2(ctx, memberRecord2) } +// todo: This will require a lot of optimizations since we iterate and make several DB calls for each chat membership func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*chatpb.GetChatsResponse, error) { log := s.log.WithField("method", "GetChats") log = client.InjectLoggingMetadata(ctx, log) @@ -124,7 +128,149 @@ func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*ch return nil, err } - return nil, status.Error(codes.Unimplemented, "") + var limit uint64 + if req.PageSize > 0 { + limit = uint64(req.PageSize) + } else { + limit = maxGetChatsPageSize + } + if limit > maxGetChatsPageSize { + limit = maxGetChatsPageSize + } + + var direction query.Ordering + if req.Direction == chatpb.GetChatsRequest_ASC { + direction = query.Ascending + } else { + direction = query.Descending + } + + var cursor query.Cursor + if req.Cursor != nil { + cursor = req.Cursor.Value + } else { + cursor = query.ToCursor(0) + if direction == query.Descending { + cursor = query.ToCursor(math.MaxInt64 - 1) + } + } + + patformUserMemberRecords, err := s.data.GetPlatformUserChatMembershipV2( + ctx, + chat.PlatformCode, // todo: support other platforms once we support revealing identity + owner.PublicKey().ToBase58(), + query.WithCursor(cursor), + query.WithDirection(direction), + query.WithLimit(limit), + ) + if err == chat.ErrMemberNotFound { + return &chatpb.GetChatsResponse{ + Result: chatpb.GetChatsResponse_NOT_FOUND, + }, nil + } else if err != nil { + log.WithError(err).Warn("failure getting chat members for platform user") + return nil, status.Error(codes.Internal, "") + } + + var protoChats []*chatpb.ChatMetadata + for _, platformUserMemberRecord := range patformUserMemberRecords { + log := log.WithField("chat_id", platformUserMemberRecord.ChatId.String()) + + chatRecord, err := s.data.GetChatByIdV2(ctx, platformUserMemberRecord.ChatId) + if err != nil { + log.WithError(err).Warn("failure getting chat record") + return nil, status.Error(codes.Internal, "") + } + + protoChat := &chatpb.ChatMetadata{ + ChatId: chatRecord.ChatId.ToProto(), + Kind: chatRecord.ChatType.ToProto(), + + IsMuted: platformUserMemberRecord.IsMuted, + IsSubscribed: !platformUserMemberRecord.IsUnsubscribed, + + Cursor: &chatpb.Cursor{Value: query.ToCursor(uint64(platformUserMemberRecord.Id))}, + } + + // Unread count calculations can be skipped for unsubscribed chats. They + // don't appear in chat history. + skipUnreadCountQuery := platformUserMemberRecord.IsUnsubscribed + + switch chatRecord.ChatType { + case chat.ChatTypeTwoWay: + protoChat.Title = "Mock Chat" // todo: proper title with localization + + protoChat.CanMute = true + protoChat.CanUnsubscribe = true + default: + return nil, status.Errorf(codes.Unimplemented, "unsupported chat type: %s", chatRecord.ChatType.String()) + } + + chatMemberRecords, err := s.data.GetAllChatMembersV2(ctx, chatRecord.ChatId) + if err != nil { + log.WithError(err).Warn("failure getting chat members") + return nil, status.Error(codes.Internal, "") + } + for _, memberRecord := range chatMemberRecords { + var identity *chatpb.ChatMemberIdentity + switch memberRecord.Platform { + case chat.PlatformCode: + case chat.PlatformTwitter: + identity = &chatpb.ChatMemberIdentity{ + Platform: memberRecord.Platform.ToProto(), + Username: memberRecord.PlatformId, + } + default: + return nil, status.Errorf(codes.Unimplemented, "unsupported platform type: %s", memberRecord.Platform.String()) + } + + var pointers []*chatpb.Pointer + for _, optionalPointer := range []struct { + kind chat.PointerType + value *chat.MessageId + }{ + {chat.PointerTypeDelivered, memberRecord.DeliveryPointer}, + {chat.PointerTypeRead, memberRecord.ReadPointer}, + } { + if optionalPointer.value == nil { + continue + } + + pointers = append(pointers, &chatpb.Pointer{ + Kind: optionalPointer.kind.ToProto(), + Value: optionalPointer.value.ToProto(), + MemberId: memberRecord.MemberId.ToProto(), + }) + } + + protoChat.Members = append(protoChat.Members, &chatpb.ChatMember{ + MemberId: memberRecord.MemberId.ToProto(), + IsSelf: bytes.Equal(memberRecord.MemberId[:], platformUserMemberRecord.MemberId[:]), + Identity: identity, + Pointers: pointers, + }) + } + + if !skipUnreadCountQuery { + readPointer := chat.GenerateMessageIdAtTime(time.Unix(0, 0)) + if platformUserMemberRecord.ReadPointer != nil { + readPointer = *platformUserMemberRecord.ReadPointer + } + unreadCount, err := s.data.GetChatUnreadCountV2(ctx, chatRecord.ChatId, readPointer) + if err != nil { + log.WithError(err).Warn("failure getting unread count") + return nil, status.Error(codes.Internal, "") + } + protoChat.NumUnread = unreadCount + } + + protoChats = append(protoChats, protoChat) + } + + return &chatpb.GetChatsResponse{ + Result: chatpb.GetChatsResponse_OK, + Chats: protoChats, + }, nil } func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest) (*chatpb.GetMessagesResponse, error) { From febc5f214d3623108219a77f08b76d96d620a329 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 13 Jun 2024 12:20:40 -0400 Subject: [PATCH 32/40] Remove addressed todo --- pkg/code/server/grpc/chat/v2/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index d0448565..f1d24916 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -38,7 +38,6 @@ const ( flushMessageCount = 100 ) -// todo: Ensure all relevant logging fields are set type server struct { log *logrus.Entry From 26bf2b71e0aff1a4da7cc263981ccf7bb3d063f0 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 13 Jun 2024 15:39:14 -0400 Subject: [PATCH 33/40] Have chat v2 store indicate if pointer was advanced --- pkg/code/data/chat/v2/memory/store.go | 29 +++++++++++++------------- pkg/code/data/chat/v2/store.go | 2 +- pkg/code/data/internal.go | 4 ++-- pkg/code/server/grpc/chat/v2/server.go | 19 +++++++++-------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/pkg/code/data/chat/v2/memory/store.go b/pkg/code/data/chat/v2/memory/store.go index 97222fab..fc17e977 100644 --- a/pkg/code/data/chat/v2/memory/store.go +++ b/pkg/code/data/chat/v2/memory/store.go @@ -201,11 +201,11 @@ func (s *store) PutMessage(_ context.Context, record *chat.MessageRecord) error } // AdvancePointer implements chat.Store.AdvancePointer -func (s *store) AdvancePointer(_ context.Context, chatId chat.ChatId, memberId chat.MemberId, pointerType chat.PointerType, pointer chat.MessageId) error { +func (s *store) AdvancePointer(_ context.Context, chatId chat.ChatId, memberId chat.MemberId, pointerType chat.PointerType, pointer chat.MessageId) (bool, error) { switch pointerType { case chat.PointerTypeDelivered, chat.PointerTypeRead: default: - return chat.ErrInvalidPointerType + return false, chat.ErrInvalidPointerType } s.mu.Lock() @@ -213,7 +213,7 @@ func (s *store) AdvancePointer(_ context.Context, chatId chat.ChatId, memberId c item := s.findMemberById(chatId, memberId) if item == nil { - return chat.ErrMemberNotFound + return false, chat.ErrMemberNotFound } var currentPointer *chat.MessageId @@ -224,20 +224,19 @@ func (s *store) AdvancePointer(_ context.Context, chatId chat.ChatId, memberId c currentPointer = item.ReadPointer } - if currentPointer != nil && currentPointer.After(pointer) { - return nil - } + if currentPointer == nil || currentPointer.Before(pointer) { + switch pointerType { + case chat.PointerTypeDelivered: + cloned := pointer.Clone() + item.DeliveryPointer = &cloned + case chat.PointerTypeRead: + cloned := pointer.Clone() + item.ReadPointer = &cloned + } - switch pointerType { - case chat.PointerTypeDelivered: - cloned := pointer.Clone() - item.DeliveryPointer = &cloned - case chat.PointerTypeRead: - cloned := pointer.Clone() - item.ReadPointer = &cloned + return true, nil } - - return nil + return false, nil } // SetMuteState implements chat.Store.SetMuteState diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index 5a0e9f20..82e8150f 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -55,7 +55,7 @@ type Store interface { PutMessage(ctx context.Context, record *MessageRecord) error // AdvancePointer advances a chat pointer for a chat member - AdvancePointer(ctx context.Context, chatId ChatId, memberId MemberId, pointerType PointerType, pointer MessageId) error + AdvancePointer(ctx context.Context, chatId ChatId, memberId MemberId, pointerType PointerType, pointer MessageId) (bool, error) // SetMuteState updates the mute state for a chat member SetMuteState(ctx context.Context, chatId ChatId, memberId MemberId, isMuted bool) error diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 68ab5816..c4a06397 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -406,7 +406,7 @@ type DatabaseData interface { PutChatV2(ctx context.Context, record *chat_v2.ChatRecord) error PutChatMemberV2(ctx context.Context, record *chat_v2.MemberRecord) error PutChatMessageV2(ctx context.Context, record *chat_v2.MessageRecord) error - AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) error + AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) (bool, error) SetChatMuteStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isMuted bool) error SetChatSubscriptionStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isSubscribed bool) error @@ -1504,7 +1504,7 @@ func (dp *DatabaseProvider) PutChatMemberV2(ctx context.Context, record *chat_v2 func (dp *DatabaseProvider) PutChatMessageV2(ctx context.Context, record *chat_v2.MessageRecord) error { return dp.chatv2.PutMessage(ctx, record) } -func (dp *DatabaseProvider) AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) error { +func (dp *DatabaseProvider) AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) (bool, error) { return dp.chatv2.AdvancePointer(ctx, chatId, memberId, pointerType, pointer) } func (dp *DatabaseProvider) SetChatMuteStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isMuted bool) error { diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index f1d24916..47287f23 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -851,20 +851,21 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR return nil, status.Error(codes.Internal, "") } - // Note: Guarantees that pointer will never be advanced to some point in the past - err = s.data.AdvanceChatPointerV2(ctx, chatId, memberId, pointerType, pointerValue) + isAdvanced, err := s.data.AdvanceChatPointerV2(ctx, chatId, memberId, pointerType, pointerValue) if err != nil { log.WithError(err).Warn("failure advancing chat pointer") return nil, status.Error(codes.Internal, "") } - event := &chatpb.ChatStreamEvent{ - Type: &chatpb.ChatStreamEvent_Pointer{ - Pointer: req.Pointer, - }, - } - if err := s.asyncNotifyAll(chatId, memberId, event); err != nil { - log.WithError(err).Warn("failure notifying chat event") + if isAdvanced { + event := &chatpb.ChatStreamEvent{ + Type: &chatpb.ChatStreamEvent_Pointer{ + Pointer: req.Pointer, + }, + } + if err := s.asyncNotifyAll(chatId, memberId, event); err != nil { + log.WithError(err).Warn("failure notifying chat event") + } } return &chatpb.AdvancePointerResponse{ From 74852ba238f209d5ee8f926aa59680858eb19e74 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 13 Jun 2024 16:54:07 -0400 Subject: [PATCH 34/40] Fix unread count to not count messages sent by the reader --- pkg/code/data/chat/v2/memory/store.go | 13 ++++++++++++- pkg/code/data/chat/v2/store.go | 4 ++-- pkg/code/data/internal.go | 6 +++--- pkg/code/server/grpc/chat/v2/server.go | 2 +- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/pkg/code/data/chat/v2/memory/store.go b/pkg/code/data/chat/v2/memory/store.go index fc17e977..f11fc44d 100644 --- a/pkg/code/data/chat/v2/memory/store.go +++ b/pkg/code/data/chat/v2/memory/store.go @@ -98,12 +98,13 @@ func (s *store) GetAllMembersByPlatformId(_ context.Context, platform chat.Platf } // GetUnreadCount implements chat.store.GetUnreadCount -func (s *store) GetUnreadCount(_ context.Context, chatId chat.ChatId, readPointer chat.MessageId) (uint32, error) { +func (s *store) GetUnreadCount(_ context.Context, chatId chat.ChatId, memberId chat.MemberId, readPointer chat.MessageId) (uint32, error) { s.mu.Lock() defer s.mu.Unlock() items := s.findMessagesByChatId(chatId) items = s.filterMessagesAfter(items, readPointer) + items = s.filterMessagesNotSentBy(items, memberId) items = s.filterNotifiedMessages(items) return uint32(len(items)), nil } @@ -414,6 +415,16 @@ func (s *store) filterMessagesAfter(items []*chat.MessageRecord, pointer chat.Me return res } +func (s *store) filterMessagesNotSentBy(items []*chat.MessageRecord, sender chat.MemberId) []*chat.MessageRecord { + var res []*chat.MessageRecord + for _, item := range items { + if item.Sender == nil || !bytes.Equal(item.Sender[:], sender[:]) { + res = append(res, item) + } + } + return res +} + func (s *store) filterNotifiedMessages(items []*chat.MessageRecord) []*chat.MessageRecord { var res []*chat.MessageRecord for _, item := range items { diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index 82e8150f..f58d34b4 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -42,8 +42,8 @@ type Store interface { // Note: Cursor is a message ID GetAllMessagesByChatId(ctx context.Context, chatId ChatId, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*MessageRecord, error) - // GetUnreadCount gets the unread message count for a chat ID at a read pointer - GetUnreadCount(ctx context.Context, chatId ChatId, readPointer MessageId) (uint32, error) + // GetUnreadCount gets the unread message count for a chat ID at a read pointer for a given chat member + GetUnreadCount(ctx context.Context, chatId ChatId, memberId MemberId, readPointer MessageId) (uint32, error) // PutChat creates a new chat PutChat(ctx context.Context, record *ChatRecord) error diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index c4a06397..93248a4c 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -402,7 +402,7 @@ type DatabaseData interface { GetAllChatMembersV2(ctx context.Context, chatId chat_v2.ChatId) ([]*chat_v2.MemberRecord, error) GetPlatformUserChatMembershipV2(ctx context.Context, platform chat_v2.Platform, platformId string, opts ...query.Option) ([]*chat_v2.MemberRecord, error) GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) - GetChatUnreadCountV2(ctx context.Context, chatId chat_v2.ChatId, readPointer chat_v2.MessageId) (uint32, error) + GetChatUnreadCountV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, readPointer chat_v2.MessageId) (uint32, error) PutChatV2(ctx context.Context, record *chat_v2.ChatRecord) error PutChatMemberV2(ctx context.Context, record *chat_v2.MemberRecord) error PutChatMessageV2(ctx context.Context, record *chat_v2.MessageRecord) error @@ -1492,8 +1492,8 @@ func (dp *DatabaseProvider) GetAllChatMessagesV2(ctx context.Context, chatId cha } return dp.chatv2.GetAllMessagesByChatId(ctx, chatId, req.Cursor, req.SortBy, req.Limit) } -func (dp *DatabaseProvider) GetChatUnreadCountV2(ctx context.Context, chatId chat_v2.ChatId, readPointer chat_v2.MessageId) (uint32, error) { - return dp.chatv2.GetUnreadCount(ctx, chatId, readPointer) +func (dp *DatabaseProvider) GetChatUnreadCountV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, readPointer chat_v2.MessageId) (uint32, error) { + return dp.chatv2.GetUnreadCount(ctx, chatId, memberId, readPointer) } func (dp *DatabaseProvider) PutChatV2(ctx context.Context, record *chat_v2.ChatRecord) error { return dp.chatv2.PutChat(ctx, record) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 47287f23..f71b7f8e 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -255,7 +255,7 @@ func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*ch if platformUserMemberRecord.ReadPointer != nil { readPointer = *platformUserMemberRecord.ReadPointer } - unreadCount, err := s.data.GetChatUnreadCountV2(ctx, chatRecord.ChatId, readPointer) + unreadCount, err := s.data.GetChatUnreadCountV2(ctx, chatRecord.ChatId, platformUserMemberRecord.MemberId, readPointer) if err != nil { log.WithError(err).Warn("failure getting unread count") return nil, status.Error(codes.Internal, "") From 8366e10388dbea712536b2617417ece29afbe2eb Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Mon, 17 Jun 2024 13:23:58 -0400 Subject: [PATCH 35/40] Fix build with refactor changes to chat protos --- go.mod | 2 +- go.sum | 4 ++-- pkg/code/data/chat/v2/model.go | 32 ++++++++++++++++---------------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/go.mod b/go.mod index e5945d8a..5a74deed 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.7-0.20240613143759-2e4fbcd11dcb + github.com/code-payments/code-protobuf-api v1.16.7-0.20240617171852-5885f1307f31 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index 1c691884..e105262d 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240613143759-2e4fbcd11dcb h1:cSgl4rZkQqQ1cDgJFGxyRZFoHljN2Of73znDpqKXftY= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240613143759-2e4fbcd11dcb/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240617171852-5885f1307f31 h1:nJ2H/SHJipF/tNJns89brkSCrZdkd9M57BqxfTqAr/s= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240617171852-5885f1307f31/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/pkg/code/data/chat/v2/model.go b/pkg/code/data/chat/v2/model.go index 9a7108f3..e0cb5ed9 100644 --- a/pkg/code/data/chat/v2/model.go +++ b/pkg/code/data/chat/v2/model.go @@ -114,11 +114,11 @@ func (a MessagesByMessageId) Less(i, j int) bool { } // GetChatTypeFromProto gets a chat type from the protobuf variant -func GetChatTypeFromProto(proto chatpb.ChatMetadata_Kind) ChatType { +func GetChatTypeFromProto(proto chatpb.ChatType) ChatType { switch proto { - case chatpb.ChatMetadata_NOTIFICATION: + case chatpb.ChatType_NOTIFICATION: return ChatTypeNotification - case chatpb.ChatMetadata_TWO_WAY: + case chatpb.ChatType_TWO_WAY: return ChatTypeTwoWay default: return ChatTypeUnknown @@ -126,14 +126,14 @@ func GetChatTypeFromProto(proto chatpb.ChatMetadata_Kind) ChatType { } // ToProto returns the proto representation of the chat type -func (c ChatType) ToProto() chatpb.ChatMetadata_Kind { +func (c ChatType) ToProto() chatpb.ChatType { switch c { case ChatTypeNotification: - return chatpb.ChatMetadata_NOTIFICATION + return chatpb.ChatType_NOTIFICATION case ChatTypeTwoWay: - return chatpb.ChatMetadata_TWO_WAY + return chatpb.ChatType_TWO_WAY default: - return chatpb.ChatMetadata_UNKNOWN + return chatpb.ChatType_UNKNOWN_CHAT_TYPE } } @@ -150,13 +150,13 @@ func (c ChatType) String() string { } // GetPointerTypeFromProto gets a chat ID from the protobuf variant -func GetPointerTypeFromProto(proto chatpb.Pointer_Kind) PointerType { +func GetPointerTypeFromProto(proto chatpb.PointerType) PointerType { switch proto { - case chatpb.Pointer_SENT: + case chatpb.PointerType_SENT: return PointerTypeSent - case chatpb.Pointer_DELIVERED: + case chatpb.PointerType_DELIVERED: return PointerTypeDelivered - case chatpb.Pointer_READ: + case chatpb.PointerType_READ: return PointerTypeRead default: return PointerTypeUnknown @@ -164,16 +164,16 @@ func GetPointerTypeFromProto(proto chatpb.Pointer_Kind) PointerType { } // ToProto returns the proto representation of the pointer type -func (p PointerType) ToProto() chatpb.Pointer_Kind { +func (p PointerType) ToProto() chatpb.PointerType { switch p { case PointerTypeSent: - return chatpb.Pointer_SENT + return chatpb.PointerType_SENT case PointerTypeDelivered: - return chatpb.Pointer_DELIVERED + return chatpb.PointerType_DELIVERED case PointerTypeRead: - return chatpb.Pointer_READ + return chatpb.PointerType_READ default: - return chatpb.Pointer_UNKNOWN + return chatpb.PointerType_UNKNOWN_POINTER_TYPE } } From f62c762f1b581593f61f155f9b1a91e4441529cc Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 18 Jun 2024 11:55:46 -0400 Subject: [PATCH 36/40] Initial implementation of StartChat that always starts a new chat --- go.mod | 2 +- go.sum | 4 +- pkg/code/data/chat/v2/memory/store.go | 15 +- pkg/code/data/chat/v2/store.go | 5 +- pkg/code/data/internal.go | 6 +- pkg/code/server/grpc/chat/v2/server.go | 463 ++++++++++++++++++------- 6 files changed, 350 insertions(+), 145 deletions(-) diff --git a/go.mod b/go.mod index 5a74deed..a06a34b9 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.7-0.20240617171852-5885f1307f31 + github.com/code-payments/code-protobuf-api v1.16.7-0.20240618135651-59879f609687 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index e105262d..176a215a 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240617171852-5885f1307f31 h1:nJ2H/SHJipF/tNJns89brkSCrZdkd9M57BqxfTqAr/s= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240617171852-5885f1307f31/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240618135651-59879f609687 h1:1/DZnT2ipA/Eo/AmFgheQomKeSFwsIAaUcxV9dOvTRg= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240618135651-59879f609687/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/pkg/code/data/chat/v2/memory/store.go b/pkg/code/data/chat/v2/memory/store.go index f11fc44d..905872f3 100644 --- a/pkg/code/data/chat/v2/memory/store.go +++ b/pkg/code/data/chat/v2/memory/store.go @@ -80,12 +80,12 @@ func (s *store) GetAllMembersByChatId(_ context.Context, chatId chat.ChatId) ([] return cloneMemberRecords(items), nil } -// GetAllMembersByPlatformId implements chat.store.GetAllMembersByPlatformId -func (s *store) GetAllMembersByPlatformId(_ context.Context, platform chat.Platform, platformId string, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MemberRecord, error) { +// GetAllMembersByPlatformIds implements chat.store.GetAllMembersByPlatformIds +func (s *store) GetAllMembersByPlatformIds(_ context.Context, idByPlatform map[chat.Platform]string, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*chat.MemberRecord, error) { s.mu.Lock() defer s.mu.Unlock() - items := s.findMembersByPlatformId(platform, platformId) + items := s.findMembersByPlatformIds(idByPlatform) items, err := s.getMemberRecordPage(items, cursor, direction, limit) if err != nil { return nil, err @@ -324,10 +324,15 @@ func (s *store) findMembersByChatId(chatId chat.ChatId) []*chat.MemberRecord { return res } -func (s *store) findMembersByPlatformId(platform chat.Platform, platformId string) []*chat.MemberRecord { +func (s *store) findMembersByPlatformIds(idByPlatform map[chat.Platform]string) []*chat.MemberRecord { var res []*chat.MemberRecord for _, item := range s.memberRecords { - if platform == item.Platform && platformId == item.PlatformId { + platformId, ok := idByPlatform[item.Platform] + if !ok { + continue + } + + if platformId == item.PlatformId { res = append(res, item) } } diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index f58d34b4..957260e8 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -33,9 +33,8 @@ type Store interface { // todo: Add paging when we introduce group chats GetAllMembersByChatId(ctx context.Context, chatId ChatId) ([]*MemberRecord, error) - // GetAllMembersByPlatformId gets all members for a given platform user across - // all chats - GetAllMembersByPlatformId(ctx context.Context, platform Platform, platformId string, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*MemberRecord, error) + // GetAllMembersByPlatformIds gets all members for platform users across all chats + GetAllMembersByPlatformIds(ctx context.Context, idByPlatform map[Platform]string, cursor query.Cursor, direction query.Ordering, limit uint64) ([]*MemberRecord, error) // GetAllMessagesByChatId gets all messages for a given chat // diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 93248a4c..11808d3e 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -400,7 +400,7 @@ type DatabaseData interface { GetChatMemberByIdV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId) (*chat_v2.MemberRecord, error) GetChatMessageByIdV2(ctx context.Context, chatId chat_v2.ChatId, messageId chat_v2.MessageId) (*chat_v2.MessageRecord, error) GetAllChatMembersV2(ctx context.Context, chatId chat_v2.ChatId) ([]*chat_v2.MemberRecord, error) - GetPlatformUserChatMembershipV2(ctx context.Context, platform chat_v2.Platform, platformId string, opts ...query.Option) ([]*chat_v2.MemberRecord, error) + GetPlatformUserChatMembershipV2(ctx context.Context, idByPlatform map[chat_v2.Platform]string, opts ...query.Option) ([]*chat_v2.MemberRecord, error) GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) GetChatUnreadCountV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, readPointer chat_v2.MessageId) (uint32, error) PutChatV2(ctx context.Context, record *chat_v2.ChatRecord) error @@ -1478,12 +1478,12 @@ func (dp *DatabaseProvider) GetChatMessageByIdV2(ctx context.Context, chatId cha func (dp *DatabaseProvider) GetAllChatMembersV2(ctx context.Context, chatId chat_v2.ChatId) ([]*chat_v2.MemberRecord, error) { return dp.chatv2.GetAllMembersByChatId(ctx, chatId) } -func (dp *DatabaseProvider) GetPlatformUserChatMembershipV2(ctx context.Context, platform chat_v2.Platform, platformId string, opts ...query.Option) ([]*chat_v2.MemberRecord, error) { +func (dp *DatabaseProvider) GetPlatformUserChatMembershipV2(ctx context.Context, idByPlatform map[chat_v2.Platform]string, opts ...query.Option) ([]*chat_v2.MemberRecord, error) { req, err := query.DefaultPaginationHandler(opts...) if err != nil { return nil, err } - return dp.chatv2.GetAllMembersByPlatformId(ctx, platform, platformId, req.Cursor, req.SortBy, req.Limit) + return dp.chatv2.GetAllMembersByPlatformIds(ctx, idByPlatform, req.Cursor, req.SortBy, req.Limit) } func (dp *DatabaseProvider) GetAllChatMessagesV2(ctx context.Context, chatId chat_v2.ChatId, opts ...query.Option) ([]*chat_v2.MessageRecord, error) { req, err := query.DefaultPaginationHandler(opts...) diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index f71b7f8e..e864ca02 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -1,13 +1,15 @@ package chat_v2 import ( - "bytes" "context" + "crypto/rand" + "database/sql" "fmt" "math" "sync" "time" + "github.com/mr-tron/base58" "github.com/pkg/errors" "github.com/sirupsen/logrus" "golang.org/x/text/language" @@ -19,11 +21,13 @@ import ( chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v2" commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1" + transactionpb "github.com/code-payments/code-protobuf-api/generated/go/transaction/v2" auth_util "github.com/code-payments/code-server/pkg/code/auth" "github.com/code-payments/code-server/pkg/code/common" code_data "github.com/code-payments/code-server/pkg/code/data" chat "github.com/code-payments/code-server/pkg/code/data/chat/v2" + "github.com/code-payments/code-server/pkg/code/data/intent" "github.com/code-payments/code-server/pkg/code/data/twitter" "github.com/code-payments/code-server/pkg/code/localization" "github.com/code-payments/code-server/pkg/database/query" @@ -70,45 +74,9 @@ func NewChatServer(data code_data.Provider, auth *auth_util.RPCSignatureVerifier go s.asyncChatEventStreamNotifier(i, channel) } - // todo: Remove when testing is complete - s.setupMockChat() - return s } -func (s *server) setupMockChat() { - ctx := context.Background() - - chatId, _ := chat.GetChatIdFromString("c355fcec8c521e7937d45283d83bbfc63a0c688004f2386a535fc817218f917b") - chatRecord := &chat.ChatRecord{ - ChatId: chatId, - ChatType: chat.ChatTypeTwoWay, - IsVerified: true, - CreatedAt: time.Now(), - } - s.data.PutChatV2(ctx, chatRecord) - - memberId1, _ := chat.GetMemberIdFromString("034dda45-b4c2-45db-b1da-181298898a16") - memberRecord1 := &chat.MemberRecord{ - ChatId: chatId, - MemberId: memberId1, - Platform: chat.PlatformCode, - PlatformId: "8bw4gaRQk91w7vtgTN4E12GnKecY2y6CjPai7WUvWBQ8", - JoinedAt: time.Now(), - } - s.data.PutChatMemberV2(ctx, memberRecord1) - - memberId2, _ := chat.GetMemberIdFromString("a9d27058-f2d8-4034-bf52-b20c09a670de") - memberRecord2 := &chat.MemberRecord{ - ChatId: chatId, - MemberId: memberId2, - Platform: chat.PlatformCode, - PlatformId: "EDknQfoUnj73L56vKtEc6Qqw5VoHaF32eHYdz3V4y27M", - JoinedAt: time.Now(), - } - s.data.PutChatMemberV2(ctx, memberRecord2) -} - // todo: This will require a lot of optimizations since we iterate and make several DB calls for each chat membership func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*chatpb.GetChatsResponse, error) { log := s.log.WithField("method", "GetChats") @@ -154,10 +122,17 @@ func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*ch } } + myIdentities, err := s.getAllIdentities(ctx, owner) + if err != nil { + log.WithError(err).Warn("failure getting identities for owner account") + return nil, status.Error(codes.Internal, "") + } + + // todo: Use a better query that returns chat IDs. This will result in duplicate + // chat results if the user is in the chat multiple times across many identities. patformUserMemberRecords, err := s.data.GetPlatformUserChatMembershipV2( ctx, - chat.PlatformCode, // todo: support other platforms once we support revealing identity - owner.PublicKey().ToBase58(), + myIdentities, query.WithCursor(cursor), query.WithDirection(direction), query.WithLimit(limit), @@ -181,87 +156,18 @@ func (s *server) GetChats(ctx context.Context, req *chatpb.GetChatsRequest) (*ch return nil, status.Error(codes.Internal, "") } - protoChat := &chatpb.ChatMetadata{ - ChatId: chatRecord.ChatId.ToProto(), - Kind: chatRecord.ChatType.ToProto(), - - IsMuted: platformUserMemberRecord.IsMuted, - IsSubscribed: !platformUserMemberRecord.IsUnsubscribed, - - Cursor: &chatpb.Cursor{Value: query.ToCursor(uint64(platformUserMemberRecord.Id))}, - } - - // Unread count calculations can be skipped for unsubscribed chats. They - // don't appear in chat history. - skipUnreadCountQuery := platformUserMemberRecord.IsUnsubscribed - - switch chatRecord.ChatType { - case chat.ChatTypeTwoWay: - protoChat.Title = "Mock Chat" // todo: proper title with localization - - protoChat.CanMute = true - protoChat.CanUnsubscribe = true - default: - return nil, status.Errorf(codes.Unimplemented, "unsupported chat type: %s", chatRecord.ChatType.String()) - } - - chatMemberRecords, err := s.data.GetAllChatMembersV2(ctx, chatRecord.ChatId) + memberRecords, err := s.data.GetAllChatMembersV2(ctx, chatRecord.ChatId) if err != nil { log.WithError(err).Warn("failure getting chat members") return nil, status.Error(codes.Internal, "") } - for _, memberRecord := range chatMemberRecords { - var identity *chatpb.ChatMemberIdentity - switch memberRecord.Platform { - case chat.PlatformCode: - case chat.PlatformTwitter: - identity = &chatpb.ChatMemberIdentity{ - Platform: memberRecord.Platform.ToProto(), - Username: memberRecord.PlatformId, - } - default: - return nil, status.Errorf(codes.Unimplemented, "unsupported platform type: %s", memberRecord.Platform.String()) - } - var pointers []*chatpb.Pointer - for _, optionalPointer := range []struct { - kind chat.PointerType - value *chat.MessageId - }{ - {chat.PointerTypeDelivered, memberRecord.DeliveryPointer}, - {chat.PointerTypeRead, memberRecord.ReadPointer}, - } { - if optionalPointer.value == nil { - continue - } - - pointers = append(pointers, &chatpb.Pointer{ - Kind: optionalPointer.kind.ToProto(), - Value: optionalPointer.value.ToProto(), - MemberId: memberRecord.MemberId.ToProto(), - }) - } - - protoChat.Members = append(protoChat.Members, &chatpb.ChatMember{ - MemberId: memberRecord.MemberId.ToProto(), - IsSelf: bytes.Equal(memberRecord.MemberId[:], platformUserMemberRecord.MemberId[:]), - Identity: identity, - Pointers: pointers, - }) - } - - if !skipUnreadCountQuery { - readPointer := chat.GenerateMessageIdAtTime(time.Unix(0, 0)) - if platformUserMemberRecord.ReadPointer != nil { - readPointer = *platformUserMemberRecord.ReadPointer - } - unreadCount, err := s.data.GetChatUnreadCountV2(ctx, chatRecord.ChatId, platformUserMemberRecord.MemberId, readPointer) - if err != nil { - log.WithError(err).Warn("failure getting unread count") - return nil, status.Error(codes.Internal, "") - } - protoChat.NumUnread = unreadCount + protoChat, err := s.toProtoChat(ctx, chatRecord, memberRecords, myIdentities) + if err != nil { + log.WithError(err).Warn("failure constructing proto chat message") + return nil, status.Error(codes.Internal, "") } + protoChat.Cursor = &chatpb.Cursor{Value: query.ToCursor(uint64(platformUserMemberRecord.Id))} protoChats = append(protoChats, protoChat) } @@ -387,7 +293,7 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e } if req.GetOpenStream() == nil { - return status.Error(codes.InvalidArgument, "open_stream is nil") + return status.Error(codes.InvalidArgument, "StreamChatEventsRequest.Type must be OpenStreamRequest") } owner, err := common.NewAccountFromProto(req.GetOpenStream().Owner) @@ -414,7 +320,7 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e signature := req.GetOpenStream().Signature req.GetOpenStream().Signature = nil if err = s.auth.Authenticate(streamer.Context(), owner, req.GetOpenStream(), signature); err != nil { - return err + // return err } _, err = s.data.GetChatByIdV2(ctx, chatId) @@ -611,6 +517,181 @@ func (s *server) flushPointers(ctx context.Context, chatId chat.ChatId, stream * } } +func (s *server) StartChat(ctx context.Context, req *chatpb.StartChatRequest) (*chatpb.StartChatResponse, error) { + log := s.log.WithField("method", "SendMessage") + log = client.InjectLoggingMetadata(ctx, log) + + owner, err := common.NewAccountFromProto(req.Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + + signature := req.Signature + req.Signature = nil + if err = s.auth.Authenticate(ctx, owner, req, signature); err != nil { + return nil, err + } + + switch typed := req.Parameters.(type) { + case *chatpb.StartChatRequest_TipChat: + intentId := base58.Encode(typed.TipChat.IntentId.Value) + log = log.WithField("intent", intentId) + + intentRecord, err := s.data.GetIntent(ctx, intentId) + if err == intent.ErrIntentNotFound { + return &chatpb.StartChatResponse{ + Result: chatpb.StartChatResponse_INVALID_PARAMETER, + Chat: nil, + }, nil + } else if err != nil { + log.WithError(err).Warn("failure getting intent record") + return nil, status.Error(codes.Internal, "") + } + + // The intent was not for a tip. + if intentRecord.SendPrivatePaymentMetadata == nil || !intentRecord.SendPrivatePaymentMetadata.IsTip { + return &chatpb.StartChatResponse{ + Result: chatpb.StartChatResponse_INVALID_PARAMETER, + Chat: nil, + }, nil + } + + tipper, err := common.NewAccountFromPublicKeyString(intentRecord.InitiatorOwnerAccount) + if err != nil { + log.WithError(err).Warn("invalid tipper owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("tipper", tipper.PublicKey().ToBase58()) + + tippee, err := common.NewAccountFromPublicKeyString(intentRecord.SendPrivatePaymentMetadata.DestinationOwnerAccount) + if err != nil { + log.WithError(err).Warn("invalid tippee owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("tippee", tippee.PublicKey().ToBase58()) + + // For now, don't allow chats where you tipped yourself. + // + // todo: How do we want to handle this case? + if owner.PublicKey().ToBase58() == tipper.PublicKey().ToBase58() { + return &chatpb.StartChatResponse{ + Result: chatpb.StartChatResponse_INVALID_PARAMETER, + Chat: nil, + }, nil + } + + // Only the owner of the platform user at the time of tipping can initiate the chat. + if owner.PublicKey().ToBase58() != tippee.PublicKey().ToBase58() { + return &chatpb.StartChatResponse{ + Result: chatpb.StartChatResponse_DENIED, + Chat: nil, + }, nil + } + + // todo: This will require a refactor when we allow creation of other types of chats + switch intentRecord.SendPrivatePaymentMetadata.TipMetadata.Platform { + case transactionpb.TippedUser_TWITTER: + twitterUsername := intentRecord.SendPrivatePaymentMetadata.TipMetadata.Username + + // The owner must still own the Twitter username + ownsUsername, err := s.ownsTwitterUsername(ctx, owner, twitterUsername) + if err != nil { + log.WithError(err).Warn("failure determing twitter username ownership") + return nil, status.Error(codes.Internal, "") + } else if !ownsUsername { + return &chatpb.StartChatResponse{ + Result: chatpb.StartChatResponse_DENIED, + }, nil + } + + // todo: try to find an existing chat, but for now always create a new completely random one + var chatId chat.ChatId + rand.Read(chatId[:]) + + creationTs := time.Now() + + chatRecord := &chat.ChatRecord{ + ChatId: chatId, + ChatType: chat.ChatTypeTwoWay, + + IsVerified: true, + + CreatedAt: creationTs, + } + + memberRecords := []*chat.MemberRecord{ + { + ChatId: chatId, + MemberId: chat.GenerateMemberId(), + + Platform: chat.PlatformTwitter, + PlatformId: twitterUsername, + + JoinedAt: creationTs, + }, + { + ChatId: chatId, + MemberId: chat.GenerateMemberId(), + + Platform: chat.PlatformCode, + PlatformId: tipper.PublicKey().ToBase58(), + + JoinedAt: creationTs, + }, + } + + err = s.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { + err := s.data.PutChatV2(ctx, chatRecord) + if err != nil { + return errors.Wrap(err, "error creating chat record") + } + + for _, memberRecord := range memberRecords { + err := s.data.PutChatMemberV2(ctx, memberRecord) + if err != nil { + return errors.Wrap(err, "error creating member record") + } + } + + return nil + }) + if err != nil { + log.WithError(err).Warn("failure creating new chat") + return nil, status.Error(codes.Internal, "") + } + + protoChat, err := s.toProtoChat( + ctx, + chatRecord, + memberRecords, + map[chat.Platform]string{ + chat.PlatformCode: owner.PublicKey().ToBase58(), + chat.PlatformTwitter: twitterUsername, + }, + ) + if err != nil { + log.WithError(err).Warn("failure constructing proto chat message") + return nil, status.Error(codes.Internal, "") + } + + return &chatpb.StartChatResponse{ + Result: chatpb.StartChatResponse_OK, + Chat: protoChat, + }, nil + default: + return &chatpb.StartChatResponse{ + Result: chatpb.StartChatResponse_INVALID_PARAMETER, + Chat: nil, + }, nil + } + + default: + return nil, status.Error(codes.InvalidArgument, "StartChatRequest.Parameters is nil") + } +} + func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest) (*chatpb.SendMessageResponse, error) { log := s.log.WithField("method", "SendMessage") log = client.InjectLoggingMetadata(ctx, log) @@ -1059,6 +1140,105 @@ func (s *server) getProtoChatMessages(ctx context.Context, chatId chat.ChatId, o return res, nil } +func (s *server) toProtoChat(ctx context.Context, chatRecord *chat.ChatRecord, memberRecords []*chat.MemberRecord, myIdentitiesByPlatform map[chat.Platform]string) (*chatpb.ChatMetadata, error) { + protoChat := &chatpb.ChatMetadata{ + ChatId: chatRecord.ChatId.ToProto(), + Kind: chatRecord.ChatType.ToProto(), + } + + switch chatRecord.ChatType { + case chat.ChatTypeTwoWay: + protoChat.Title = "Tip Chat" // todo: proper title with localization + + protoChat.CanMute = true + protoChat.CanUnsubscribe = true + default: + return nil, errors.Errorf("unsupported chat type: %s", chatRecord.ChatType.String()) + } + + for _, memberRecord := range memberRecords { + var isSelf bool + var identity *chatpb.ChatMemberIdentity + switch memberRecord.Platform { + case chat.PlatformCode: + myPublicKey, ok := myIdentitiesByPlatform[chat.PlatformCode] + isSelf = ok && myPublicKey == memberRecord.PlatformId + case chat.PlatformTwitter: + myTwitterUsername, ok := myIdentitiesByPlatform[chat.PlatformTwitter] + isSelf = ok && myTwitterUsername == memberRecord.PlatformId + + identity = &chatpb.ChatMemberIdentity{ + Platform: memberRecord.Platform.ToProto(), + Username: memberRecord.PlatformId, + } + default: + return nil, errors.Errorf("unsupported platform type: %s", memberRecord.Platform.String()) + } + + var pointers []*chatpb.Pointer + for _, optionalPointer := range []struct { + kind chat.PointerType + value *chat.MessageId + }{ + {chat.PointerTypeDelivered, memberRecord.DeliveryPointer}, + {chat.PointerTypeRead, memberRecord.ReadPointer}, + } { + if optionalPointer.value == nil { + continue + } + + pointers = append(pointers, &chatpb.Pointer{ + Kind: optionalPointer.kind.ToProto(), + Value: optionalPointer.value.ToProto(), + MemberId: memberRecord.MemberId.ToProto(), + }) + } + + protoMember := &chatpb.ChatMember{ + MemberId: memberRecord.MemberId.ToProto(), + IsSelf: isSelf, + Identity: identity, + Pointers: pointers, + } + if protoMember.IsSelf { + protoMember.IsMuted = memberRecord.IsMuted + protoMember.IsSubscribed = !memberRecord.IsUnsubscribed + + if !memberRecord.IsUnsubscribed { + readPointer := chat.GenerateMessageIdAtTime(time.Unix(0, 0)) + if memberRecord.ReadPointer != nil { + readPointer = *memberRecord.ReadPointer + } + unreadCount, err := s.data.GetChatUnreadCountV2(ctx, chatRecord.ChatId, memberRecord.MemberId, readPointer) + if err != nil { + return nil, errors.Wrap(err, "error calculating unread count") + } + protoMember.NumUnread = unreadCount + } + } + + protoChat.Members = append(protoChat.Members, protoMember) + } + + return protoChat, nil +} + +func (s *server) getAllIdentities(ctx context.Context, owner *common.Account) (map[chat.Platform]string, error) { + identities := map[chat.Platform]string{ + chat.PlatformCode: owner.PublicKey().ToBase58(), + } + + twitterUserame, ok, err := s.getOwnedTwitterUsername(ctx, owner) + if err != nil { + return nil, err + } + if ok { + identities[chat.PlatformTwitter] = twitterUserame + } + + return identities, nil +} + func (s *server) ownsChatMember(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, owner *common.Account) (bool, error) { memberRecord, err := s.data.GetChatMemberByIdV2(ctx, chatId, memberId) switch err { @@ -1073,24 +1253,45 @@ func (s *server) ownsChatMember(ctx context.Context, chatId chat.ChatId, memberI case chat.PlatformCode: return memberRecord.PlatformId == owner.PublicKey().ToBase58(), nil case chat.PlatformTwitter: - // todo: This logic should live elsewhere in somewhere more common + return s.ownsTwitterUsername(ctx, owner, memberRecord.PlatformId) + default: + return false, nil + } +} - ownerTipAccount, err := owner.ToTimelockVault(timelock_token.DataVersion1, common.KinMintAccount) - if err != nil { - return false, errors.Wrap(err, "error deriving twitter tip address") - } +// todo: This logic should live elsewhere in somewhere more common +func (s *server) ownsTwitterUsername(ctx context.Context, owner *common.Account, username string) (bool, error) { + ownerTipAccount, err := owner.ToTimelockVault(timelock_token.DataVersion1, common.KinMintAccount) + if err != nil { + return false, errors.Wrap(err, "error deriving twitter tip address") + } - twitterRecord, err := s.data.GetTwitterUserByUsername(ctx, memberRecord.PlatformId) - switch err { - case nil: - case twitter.ErrUserNotFound: - return false, nil - default: - return false, errors.Wrap(err, "error getting twitter user") - } + twitterRecord, err := s.data.GetTwitterUserByUsername(ctx, username) + switch err { + case nil: + case twitter.ErrUserNotFound: + return false, nil + default: + return false, errors.Wrap(err, "error getting twitter user") + } - return twitterRecord.TipAddress == ownerTipAccount.PublicKey().ToBase58(), nil + return twitterRecord.TipAddress == ownerTipAccount.PublicKey().ToBase58(), nil +} + +// todo: This logic should live elsewhere in somewhere more common +func (s *server) getOwnedTwitterUsername(ctx context.Context, owner *common.Account) (string, bool, error) { + ownerTipAccount, err := owner.ToTimelockVault(timelock_token.DataVersion1, common.KinMintAccount) + if err != nil { + return "", false, errors.Wrap(err, "error deriving twitter tip address") + } + + twitterRecord, err := s.data.GetTwitterUserByTipAddress(ctx, ownerTipAccount.PublicKey().ToBase58()) + switch err { + case nil: + return twitterRecord.Username, true, nil + case twitter.ErrUserNotFound: + return "", false, nil default: - return false, nil + return "", false, errors.Wrap(err, "error getting twitter user") } } From 5f39c7287183065f67c746d9390e3d16a8f58e75 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Tue, 18 Jun 2024 13:32:55 -0400 Subject: [PATCH 37/40] Initial implementation of the RevealIdentity RPC --- go.mod | 2 +- go.sum | 4 +- pkg/code/data/chat/v2/memory/store.go | 27 ++++ pkg/code/data/chat/v2/model.go | 16 ++- pkg/code/data/chat/v2/store.go | 18 ++- pkg/code/data/internal.go | 4 + pkg/code/server/grpc/chat/v2/server.go | 189 +++++++++++++++++++++++-- pkg/code/server/grpc/chat/v2/stream.go | 4 - 8 files changed, 238 insertions(+), 26 deletions(-) diff --git a/go.mod b/go.mod index a06a34b9..c8180d5c 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.7-0.20240618135651-59879f609687 + github.com/code-payments/code-protobuf-api v1.16.7-0.20240618171829-bb587a7f24ce github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index 176a215a..9fec7bb9 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240618135651-59879f609687 h1:1/DZnT2ipA/Eo/AmFgheQomKeSFwsIAaUcxV9dOvTRg= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240618135651-59879f609687/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240618171829-bb587a7f24ce h1:wdPvuDHfEmymPn4zSaVD2ymK4pRKHlegAhTnl52d/IY= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240618171829-bb587a7f24ce/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/pkg/code/data/chat/v2/memory/store.go b/pkg/code/data/chat/v2/memory/store.go index 905872f3..ff54f9ac 100644 --- a/pkg/code/data/chat/v2/memory/store.go +++ b/pkg/code/data/chat/v2/memory/store.go @@ -7,6 +7,8 @@ import ( "sync" "time" + "github.com/pkg/errors" + chat "github.com/code-payments/code-server/pkg/code/data/chat/v2" "github.com/code-payments/code-server/pkg/database/query" ) @@ -240,6 +242,31 @@ func (s *store) AdvancePointer(_ context.Context, chatId chat.ChatId, memberId c return false, nil } +// UpgradeIdentity implements chat.Store.UpgradeIdentity +func (s *store) UpgradeIdentity(_ context.Context, chatId chat.ChatId, memberId chat.MemberId, platform chat.Platform, platformId string) error { + switch platform { + case chat.PlatformTwitter: + default: + return errors.Errorf("platform not supported for identity upgrades: %s", platform.String()) + } + + s.mu.Lock() + defer s.mu.Unlock() + + item := s.findMemberById(chatId, memberId) + if item == nil { + return chat.ErrMemberNotFound + } + if item.Platform != chat.PlatformCode { + return chat.ErrMemberIdentityAlreadyUpgraded + } + + item.Platform = platform + item.PlatformId = platformId + + return nil +} + // SetMuteState implements chat.Store.SetMuteState func (s *store) SetMuteState(_ context.Context, chatId chat.ChatId, memberId chat.MemberId, isMuted bool) error { s.mu.Lock() diff --git a/pkg/code/data/chat/v2/model.go b/pkg/code/data/chat/v2/model.go index e0cb5ed9..ef3c7071 100644 --- a/pkg/code/data/chat/v2/model.go +++ b/pkg/code/data/chat/v2/model.go @@ -192,12 +192,22 @@ func (p PointerType) String() string { } // ToProto returns the proto representation of the platform -func (p Platform) ToProto() chatpb.ChatMemberIdentity_Platform { +func GetPlatformFromProto(proto chatpb.Platform) Platform { + switch proto { + case chatpb.Platform_TWITTER: + return PlatformTwitter + default: + return PlatformUnknown + } +} + +// ToProto returns the proto representation of the platform +func (p Platform) ToProto() chatpb.Platform { switch p { case PlatformTwitter: - return chatpb.ChatMemberIdentity_TWITTER + return chatpb.Platform_TWITTER default: - return chatpb.ChatMemberIdentity_UNKNOWN + return chatpb.Platform_UNKNOWN_PLATFORM } } diff --git a/pkg/code/data/chat/v2/store.go b/pkg/code/data/chat/v2/store.go index 957260e8..a3fc4b43 100644 --- a/pkg/code/data/chat/v2/store.go +++ b/pkg/code/data/chat/v2/store.go @@ -8,13 +8,14 @@ import ( ) var ( - ErrChatExists = errors.New("chat already exists") - ErrChatNotFound = errors.New("chat not found") - ErrMemberExists = errors.New("chat member already exists") - ErrMemberNotFound = errors.New("chat member not found") - ErrMessageExsits = errors.New("chat message already exists") - ErrMessageNotFound = errors.New("chat message not found") - ErrInvalidPointerType = errors.New("invalid pointer type") + ErrChatExists = errors.New("chat already exists") + ErrChatNotFound = errors.New("chat not found") + ErrMemberExists = errors.New("chat member already exists") + ErrMemberNotFound = errors.New("chat member not found") + ErrMemberIdentityAlreadyUpgraded = errors.New("chat member identity already upgraded") + ErrMessageExsits = errors.New("chat message already exists") + ErrMessageNotFound = errors.New("chat message not found") + ErrInvalidPointerType = errors.New("invalid pointer type") ) // todo: Define interface methods @@ -56,6 +57,9 @@ type Store interface { // AdvancePointer advances a chat pointer for a chat member AdvancePointer(ctx context.Context, chatId ChatId, memberId MemberId, pointerType PointerType, pointer MessageId) (bool, error) + // UpgradeIdentity upgrades a chat member's identity from an anonymous state + UpgradeIdentity(ctx context.Context, chatId ChatId, memberId MemberId, platform Platform, platformId string) error + // SetMuteState updates the mute state for a chat member SetMuteState(ctx context.Context, chatId ChatId, memberId MemberId, isMuted bool) error diff --git a/pkg/code/data/internal.go b/pkg/code/data/internal.go index 11808d3e..c043a8f2 100644 --- a/pkg/code/data/internal.go +++ b/pkg/code/data/internal.go @@ -407,6 +407,7 @@ type DatabaseData interface { PutChatMemberV2(ctx context.Context, record *chat_v2.MemberRecord) error PutChatMessageV2(ctx context.Context, record *chat_v2.MessageRecord) error AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) (bool, error) + UpgradeChatMemberIdentityV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, platform chat_v2.Platform, platformId string) error SetChatMuteStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isMuted bool) error SetChatSubscriptionStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isSubscribed bool) error @@ -1507,6 +1508,9 @@ func (dp *DatabaseProvider) PutChatMessageV2(ctx context.Context, record *chat_v func (dp *DatabaseProvider) AdvanceChatPointerV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, pointerType chat_v2.PointerType, pointer chat_v2.MessageId) (bool, error) { return dp.chatv2.AdvancePointer(ctx, chatId, memberId, pointerType, pointer) } +func (dp *DatabaseProvider) UpgradeChatMemberIdentityV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, platform chat_v2.Platform, platformId string) error { + return dp.chatv2.UpgradeIdentity(ctx, chatId, memberId, platform, platformId) +} func (dp *DatabaseProvider) SetChatMuteStateV2(ctx context.Context, chatId chat_v2.ChatId, memberId chat_v2.MemberId, isMuted bool) error { return dp.chatv2.SetMuteState(ctx, chatId, memberId, isMuted) } diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index e864ca02..021608f0 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -36,6 +36,8 @@ import ( sync_util "github.com/code-payments/code-server/pkg/sync" ) +// todo: resolve some common code for sending chat messages across RPCs + const ( maxGetChatsPageSize = 100 maxGetMessagesPageSize = 100 @@ -221,7 +223,7 @@ func (s *server) GetMessages(ctx context.Context, req *chatpb.GetMessagesRequest return nil, status.Error(codes.Internal, "") } - ownsChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + ownsChatMember, err := s.ownsChatMemberWithoutRecord(ctx, chatId, memberId, owner) if err != nil { log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") @@ -337,7 +339,7 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e return status.Error(codes.Internal, "") } - ownsChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + ownsChatMember, err := s.ownsChatMemberWithoutRecord(ctx, chatId, memberId, owner) if err != nil { log.WithError(err).Warn("failure determing chat member ownership") return status.Error(codes.Internal, "") @@ -518,7 +520,7 @@ func (s *server) flushPointers(ctx context.Context, chatId chat.ChatId, stream * } func (s *server) StartChat(ctx context.Context, req *chatpb.StartChatRequest) (*chatpb.StartChatResponse, error) { - log := s.log.WithField("method", "SendMessage") + log := s.log.WithField("method", "StartChat") log = client.InjectLoggingMetadata(ctx, log) owner, err := common.NewAccountFromProto(req.Owner) @@ -751,7 +753,7 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest }, nil } - ownsChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + ownsChatMember, err := s.ownsChatMemberWithoutRecord(ctx, chatId, memberId, owner) if err != nil { log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") @@ -910,7 +912,7 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR return nil, status.Error(codes.Internal, "") } - ownsChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + ownsChatMember, err := s.ownsChatMemberWithoutRecord(ctx, chatId, memberId, owner) if err != nil { log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") @@ -954,6 +956,171 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR }, nil } +func (s *server) RevealIdentity(ctx context.Context, req *chatpb.RevealIdentityRequest) (*chatpb.RevealIdentityResponse, error) { + log := s.log.WithField("method", "RevealIdentity") + log = client.InjectLoggingMetadata(ctx, log) + + owner, err := common.NewAccountFromProto(req.Owner) + if err != nil { + log.WithError(err).Warn("invalid owner account") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("owner_account", owner.PublicKey().ToBase58()) + + chatId, err := chat.GetChatIdFromProto(req.ChatId) + if err != nil { + log.WithError(err).Warn("invalid chat id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("chat_id", chatId.String()) + + memberId, err := chat.GetMemberIdFromProto(req.MemberId) + if err != nil { + log.WithError(err).Warn("invalid member id") + return nil, status.Error(codes.Internal, "") + } + log = log.WithField("member_id", memberId.String()) + + signature := req.Signature + req.Signature = nil + if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { + return nil, err + } + + platform := chat.GetPlatformFromProto(req.Identity.Platform) + + log = log.WithFields(logrus.Fields{ + "platform": platform.String(), + "username": req.Identity.Username, + }) + + _, err = s.data.GetChatByIdV2(ctx, chatId) + switch err { + case nil: + case chat.ErrChatNotFound: + return &chatpb.RevealIdentityResponse{ + Result: chatpb.RevealIdentityResponse_CHAT_NOT_FOUND, + }, nil + default: + log.WithError(err).Warn("failure getting chat record") + return nil, status.Error(codes.Internal, "") + } + + memberRecord, err := s.data.GetChatMemberByIdV2(ctx, chatId, memberId) + switch err { + case nil: + case chat.ErrMemberNotFound: + return &chatpb.RevealIdentityResponse{ + Result: chatpb.RevealIdentityResponse_DENIED, + }, nil + default: + log.WithError(err).Warn("failure getting member record") + return nil, status.Error(codes.Internal, "") + } + + ownsChatMember, err := s.ownsChatMemberWithRecord(ctx, chatId, memberRecord, owner) + if err != nil { + log.WithError(err).Warn("failure determing chat member ownership") + return nil, status.Error(codes.Internal, "") + } else if !ownsChatMember { + return &chatpb.RevealIdentityResponse{ + Result: chatpb.RevealIdentityResponse_DENIED, + }, nil + } + + switch platform { + case chat.PlatformTwitter: + ownsUsername, err := s.ownsTwitterUsername(ctx, owner, req.Identity.Username) + if err != nil { + log.WithError(err).Warn("failure determing twitter username ownership") + return nil, status.Error(codes.Internal, "") + } else if !ownsUsername { + return &chatpb.RevealIdentityResponse{ + Result: chatpb.RevealIdentityResponse_DENIED, + }, nil + } + default: + return nil, status.Error(codes.InvalidArgument, "RevealIdentityRequest.Identity.Platform must be TWITTER") + } + + // Idempotent RPC call using the same platform and username + if memberRecord.Platform == platform && memberRecord.PlatformId == req.Identity.Username { + return &chatpb.RevealIdentityResponse{ + Result: chatpb.RevealIdentityResponse_OK, + }, nil + } + + // Identity was already revealed, and it isn't the specified platform and username + if memberRecord.Platform != chat.PlatformCode { + return &chatpb.RevealIdentityResponse{ + Result: chatpb.RevealIdentityResponse_DIFFERENT_IDENTITY_REVEALED, + }, nil + } + + chatLock := s.chatLocks.Get(chatId[:]) + chatLock.Lock() + defer chatLock.Unlock() + + messageId := chat.GenerateMessageId() + ts, _ := messageId.GetTimestamp() + + chatMessage := &chatpb.ChatMessage{ + MessageId: messageId.ToProto(), + SenderId: req.MemberId, + Content: []*chatpb.Content{ + { + Type: &chatpb.Content_IdentityRevealed{}, + }, + }, + Ts: timestamppb.New(ts), + Cursor: &chatpb.Cursor{Value: messageId[:]}, + } + + err = s.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { + err = s.data.UpgradeChatMemberIdentityV2(ctx, chatId, memberId, platform, req.Identity.Username) + switch err { + case nil: + case chat.ErrMemberIdentityAlreadyUpgraded: + return err + default: + return errors.Wrap(err, "error updating chat member identity") + } + + err := s.persistChatMessage(ctx, chatId, chatMessage) + if err != nil { + return errors.Wrap(err, "error persisting chat message") + } + return nil + }) + + if err == nil { + event := &chatpb.ChatStreamEvent{ + Type: &chatpb.ChatStreamEvent_Message{ + Message: chatMessage, + }, + } + if err := s.asyncNotifyAll(chatId, memberId, event); err != nil { + log.WithError(err).Warn("failure notifying chat event") + } + + // todo: send the push + } + + switch err { + case nil: + return &chatpb.RevealIdentityResponse{ + Result: chatpb.RevealIdentityResponse_OK, + }, nil + case chat.ErrMemberIdentityAlreadyUpgraded: + return &chatpb.RevealIdentityResponse{ + Result: chatpb.RevealIdentityResponse_DIFFERENT_IDENTITY_REVEALED, + }, nil + default: + log.WithError(err).Warn("failure upgrading chat member identity") + return nil, status.Error(codes.Internal, "") + } +} + func (s *server) SetMuteState(ctx context.Context, req *chatpb.SetMuteStateRequest) (*chatpb.SetMuteStateResponse, error) { log := s.log.WithField("method", "SetMuteState") log = client.InjectLoggingMetadata(ctx, log) @@ -998,11 +1165,11 @@ func (s *server) SetMuteState(ctx context.Context, req *chatpb.SetMuteStateReque return nil, status.Error(codes.Internal, "") } - isChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + ownsChatMember, err := s.ownsChatMemberWithoutRecord(ctx, chatId, memberId, owner) if err != nil { log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") - } else if !isChatMember { + } else if !ownsChatMember { return &chatpb.SetMuteStateResponse{ Result: chatpb.SetMuteStateResponse_DENIED, }, nil @@ -1063,7 +1230,7 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr return nil, status.Error(codes.Internal, "") } - ownsChatMember, err := s.ownsChatMember(ctx, chatId, memberId, owner) + ownsChatMember, err := s.ownsChatMemberWithoutRecord(ctx, chatId, memberId, owner) if err != nil { log.WithError(err).Warn("failure determing chat member ownership") return nil, status.Error(codes.Internal, "") @@ -1239,7 +1406,7 @@ func (s *server) getAllIdentities(ctx context.Context, owner *common.Account) (m return identities, nil } -func (s *server) ownsChatMember(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, owner *common.Account) (bool, error) { +func (s *server) ownsChatMemberWithoutRecord(ctx context.Context, chatId chat.ChatId, memberId chat.MemberId, owner *common.Account) (bool, error) { memberRecord, err := s.data.GetChatMemberByIdV2(ctx, chatId, memberId) switch err { case nil: @@ -1249,6 +1416,10 @@ func (s *server) ownsChatMember(ctx context.Context, chatId chat.ChatId, memberI return false, errors.Wrap(err, "error getting member record") } + return s.ownsChatMemberWithRecord(ctx, chatId, memberRecord, owner) +} + +func (s *server) ownsChatMemberWithRecord(ctx context.Context, chatId chat.ChatId, memberRecord *chat.MemberRecord, owner *common.Account) (bool, error) { switch memberRecord.Platform { case chat.PlatformCode: return memberRecord.PlatformId == owner.PublicKey().ToBase58(), nil diff --git a/pkg/code/server/grpc/chat/v2/stream.go b/pkg/code/server/grpc/chat/v2/stream.go index ec797a77..17d69b6a 100644 --- a/pkg/code/server/grpc/chat/v2/stream.go +++ b/pkg/code/server/grpc/chat/v2/stream.go @@ -133,10 +133,6 @@ func (s *server) asyncChatEventStreamNotifier(workerId int, channel <-chan inter continue } - if strings.HasSuffix(key, typedValue.memberId.String()) { - continue - } - if err := stream.notify(typedValue.event, streamNotifyTimeout); err != nil { log.WithError(err).Warnf("failed to notify session stream, closing streamer (stream=%p)", stream) } From c61f7b0518ae91470dc467b513f598f5b7031811 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Wed, 19 Jun 2024 09:54:18 -0400 Subject: [PATCH 38/40] More refactors of chat stuff --- go.mod | 2 +- go.sum | 4 +- pkg/code/server/grpc/chat/v2/server.go | 203 ++++++++++++------------- pkg/code/server/grpc/chat/v2/stream.go | 11 +- 4 files changed, 108 insertions(+), 112 deletions(-) diff --git a/go.mod b/go.mod index c8180d5c..dac3ff65 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.7-0.20240618171829-bb587a7f24ce + github.com/code-payments/code-protobuf-api v1.16.7-0.20240619134635-bde7104d4467 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index 9fec7bb9..1ad4c9a3 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240618171829-bb587a7f24ce h1:wdPvuDHfEmymPn4zSaVD2ymK4pRKHlegAhTnl52d/IY= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240618171829-bb587a7f24ce/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240619134635-bde7104d4467 h1:BQgr91ASdCdxs8loz54TzmZQ0e6gDk1T7pkQxChJpdg= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240619134635-bde7104d4467/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 021608f0..09abdabc 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -321,8 +321,8 @@ func (s *server) StreamChatEvents(streamer chatpb.Chat_StreamChatEventsServer) e signature := req.GetOpenStream().Signature req.GetOpenStream().Signature = nil - if err = s.auth.Authenticate(streamer.Context(), owner, req.GetOpenStream(), signature); err != nil { - // return err + if err := s.auth.Authenticate(streamer.Context(), owner, req.GetOpenStream(), signature); err != nil { + return err } _, err = s.data.GetChatByIdV2(ctx, chatId) @@ -532,7 +532,7 @@ func (s *server) StartChat(ctx context.Context, req *chatpb.StartChatRequest) (* signature := req.Signature req.Signature = nil - if err = s.auth.Authenticate(ctx, owner, req, signature); err != nil { + if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { return nil, err } @@ -729,7 +729,7 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest signature := req.Signature req.Signature = nil - if err = s.auth.Authenticate(ctx, owner, req, signature); err != nil { + if err := s.auth.Authenticate(ctx, owner, req, signature); err != nil { return nil, err } @@ -767,16 +767,7 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest chatLock.Lock() defer chatLock.Unlock() - messageId := chat.GenerateMessageId() - ts, _ := messageId.GetTimestamp() - - chatMessage := &chatpb.ChatMessage{ - MessageId: messageId.ToProto(), - SenderId: req.MemberId, - Content: req.Content, - Ts: timestamppb.New(ts), - Cursor: &chatpb.Cursor{Value: messageId[:]}, - } + chatMessage := newProtoChatMessage(memberId, req.Content...) err = s.persistChatMessage(ctx, chatId, chatMessage) if err != nil { @@ -784,16 +775,7 @@ func (s *server) SendMessage(ctx context.Context, req *chatpb.SendMessageRequest return nil, status.Error(codes.Internal, "") } - event := &chatpb.ChatStreamEvent{ - Type: &chatpb.ChatStreamEvent_Message{ - Message: chatMessage, - }, - } - if err := s.asyncNotifyAll(chatId, memberId, event); err != nil { - log.WithError(err).Warn("failure notifying chat event") - } - - // todo: send the push + s.onPersistChatMessage(log, chatId, chatMessage) return &chatpb.SendMessageResponse{ Result: chatpb.SendMessageResponse_OK, @@ -946,7 +928,7 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR Pointer: req.Pointer, }, } - if err := s.asyncNotifyAll(chatId, memberId, event); err != nil { + if err := s.asyncNotifyAll(chatId, event); err != nil { log.WithError(err).Warn("failure notifying chat event") } } @@ -1061,20 +1043,17 @@ func (s *server) RevealIdentity(ctx context.Context, req *chatpb.RevealIdentityR chatLock.Lock() defer chatLock.Unlock() - messageId := chat.GenerateMessageId() - ts, _ := messageId.GetTimestamp() - - chatMessage := &chatpb.ChatMessage{ - MessageId: messageId.ToProto(), - SenderId: req.MemberId, - Content: []*chatpb.Content{ - { - Type: &chatpb.Content_IdentityRevealed{}, + chatMessage := newProtoChatMessage( + memberId, + &chatpb.Content{ + Type: &chatpb.Content_IdentityRevealed{ + IdentityRevealed: &chatpb.IdentityRevealedContent{ + MemberId: req.MemberId, + Identity: req.Identity, + }, }, }, - Ts: timestamppb.New(ts), - Cursor: &chatpb.Cursor{Value: messageId[:]}, - } + ) err = s.data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error { err = s.data.UpgradeChatMemberIdentityV2(ctx, chatId, memberId, platform, req.Identity.Username) @@ -1094,16 +1073,7 @@ func (s *server) RevealIdentity(ctx context.Context, req *chatpb.RevealIdentityR }) if err == nil { - event := &chatpb.ChatStreamEvent{ - Type: &chatpb.ChatStreamEvent_Message{ - Message: chatMessage, - }, - } - if err := s.asyncNotifyAll(chatId, memberId, event); err != nil { - log.WithError(err).Warn("failure notifying chat event") - } - - // todo: send the push + s.onPersistChatMessage(log, chatId, chatMessage) } switch err { @@ -1251,66 +1221,11 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr }, nil } -func (s *server) getProtoChatMessages(ctx context.Context, chatId chat.ChatId, owner *common.Account, queryOptions ...query.Option) ([]*chatpb.ChatMessage, error) { - messageRecords, err := s.data.GetAllChatMessagesV2( - ctx, - chatId, - queryOptions..., - ) - if err == chat.ErrMessageNotFound { - return nil, err - } - - var userLocale *language.Tag // Loaded lazily when required - var res []*chatpb.ChatMessage - for _, messageRecord := range messageRecords { - var protoChatMessage chatpb.ChatMessage - err = proto.Unmarshal(messageRecord.Data, &protoChatMessage) - if err != nil { - return nil, errors.Wrap(err, "error unmarshalling proto chat message") - } - - ts, err := messageRecord.GetTimestamp() - if err != nil { - return nil, errors.Wrap(err, "error getting message timestamp") - } - - for _, content := range protoChatMessage.Content { - switch typed := content.Type.(type) { - case *chatpb.Content_Localized: - if userLocale == nil { - loadedUserLocale, err := s.data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) - if err != nil { - return nil, errors.Wrap(err, "error getting user locale") - } - userLocale = &loadedUserLocale - } - - typed.Localized.KeyOrText = localization.LocalizeWithFallback( - *userLocale, - localization.GetLocalizationKeyForUserAgent(ctx, typed.Localized.KeyOrText), - typed.Localized.KeyOrText, - ) - } - } - - protoChatMessage.MessageId = messageRecord.MessageId.ToProto() - if messageRecord.Sender != nil { - protoChatMessage.SenderId = messageRecord.Sender.ToProto() - } - protoChatMessage.Ts = timestamppb.New(ts) - protoChatMessage.Cursor = &chatpb.Cursor{Value: messageRecord.MessageId[:]} - - res = append(res, &protoChatMessage) - } - - return res, nil -} - func (s *server) toProtoChat(ctx context.Context, chatRecord *chat.ChatRecord, memberRecords []*chat.MemberRecord, myIdentitiesByPlatform map[chat.Platform]string) (*chatpb.ChatMetadata, error) { protoChat := &chatpb.ChatMetadata{ ChatId: chatRecord.ChatId.ToProto(), Kind: chatRecord.ChatType.ToProto(), + Cursor: &chatpb.Cursor{Value: query.ToCursor(uint64(chatRecord.Id))}, } switch chatRecord.ChatType { @@ -1390,6 +1305,75 @@ func (s *server) toProtoChat(ctx context.Context, chatRecord *chat.ChatRecord, m return protoChat, nil } +func (s *server) getProtoChatMessages(ctx context.Context, chatId chat.ChatId, owner *common.Account, queryOptions ...query.Option) ([]*chatpb.ChatMessage, error) { + messageRecords, err := s.data.GetAllChatMessagesV2( + ctx, + chatId, + queryOptions..., + ) + if err == chat.ErrMessageNotFound { + return nil, err + } + + var userLocale *language.Tag // Loaded lazily when required + var res []*chatpb.ChatMessage + for _, messageRecord := range messageRecords { + var protoChatMessage chatpb.ChatMessage + err = proto.Unmarshal(messageRecord.Data, &protoChatMessage) + if err != nil { + return nil, errors.Wrap(err, "error unmarshalling proto chat message") + } + + ts, err := messageRecord.GetTimestamp() + if err != nil { + return nil, errors.Wrap(err, "error getting message timestamp") + } + + for _, content := range protoChatMessage.Content { + switch typed := content.Type.(type) { + case *chatpb.Content_Localized: + if userLocale == nil { + loadedUserLocale, err := s.data.GetUserLocale(ctx, owner.PublicKey().ToBase58()) + if err != nil { + return nil, errors.Wrap(err, "error getting user locale") + } + userLocale = &loadedUserLocale + } + + typed.Localized.KeyOrText = localization.LocalizeWithFallback( + *userLocale, + localization.GetLocalizationKeyForUserAgent(ctx, typed.Localized.KeyOrText), + typed.Localized.KeyOrText, + ) + } + } + + protoChatMessage.MessageId = messageRecord.MessageId.ToProto() + if messageRecord.Sender != nil { + protoChatMessage.SenderId = messageRecord.Sender.ToProto() + } + protoChatMessage.Ts = timestamppb.New(ts) + protoChatMessage.Cursor = &chatpb.Cursor{Value: messageRecord.MessageId[:]} + + res = append(res, &protoChatMessage) + } + + return res, nil +} + +func (s *server) onPersistChatMessage(log *logrus.Entry, chatId chat.ChatId, chatMessage *chatpb.ChatMessage) { + event := &chatpb.ChatStreamEvent{ + Type: &chatpb.ChatStreamEvent_Message{ + Message: chatMessage, + }, + } + if err := s.asyncNotifyAll(chatId, event); err != nil { + log.WithError(err).Warn("failure notifying chat event") + } + + // todo: send the push +} + func (s *server) getAllIdentities(ctx context.Context, owner *common.Account) (map[chat.Platform]string, error) { identities := map[chat.Platform]string{ chat.PlatformCode: owner.PublicKey().ToBase58(), @@ -1466,3 +1450,16 @@ func (s *server) getOwnedTwitterUsername(ctx context.Context, owner *common.Acco return "", false, errors.Wrap(err, "error getting twitter user") } } + +func newProtoChatMessage(sender chat.MemberId, content ...*chatpb.Content) *chatpb.ChatMessage { + messageId := chat.GenerateMessageId() + ts, _ := messageId.GetTimestamp() + + return &chatpb.ChatMessage{ + MessageId: messageId.ToProto(), + SenderId: sender.ToProto(), + Content: content, + Ts: timestamppb.New(ts), + Cursor: &chatpb.Cursor{Value: messageId[:]}, + } +} diff --git a/pkg/code/server/grpc/chat/v2/stream.go b/pkg/code/server/grpc/chat/v2/stream.go index 17d69b6a..3d39428d 100644 --- a/pkg/code/server/grpc/chat/v2/stream.go +++ b/pkg/code/server/grpc/chat/v2/stream.go @@ -93,15 +93,14 @@ func boundedStreamChatEventsRecv( } type chatEventNotification struct { - chatId chat.ChatId - memberId chat.MemberId - event *chatpb.ChatStreamEvent - ts time.Time + chatId chat.ChatId + event *chatpb.ChatStreamEvent + ts time.Time } -func (s *server) asyncNotifyAll(chatId chat.ChatId, memberId chat.MemberId, event *chatpb.ChatStreamEvent) error { +func (s *server) asyncNotifyAll(chatId chat.ChatId, event *chatpb.ChatStreamEvent) error { m := proto.Clone(event).(*chatpb.ChatStreamEvent) - ok := s.chatEventChans.Send(chatId[:], &chatEventNotification{chatId, memberId, m, time.Now()}) + ok := s.chatEventChans.Send(chatId[:], &chatEventNotification{chatId, m, time.Now()}) if !ok { return errors.New("chat event channel is full") } From 9e74ca3ea5b3954b7440b5a2219eed188f8d02b9 Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 20 Jun 2024 13:58:28 -0400 Subject: [PATCH 39/40] Incorporate small tweaks to chat APIs --- go.mod | 2 +- go.sum | 4 ++-- pkg/code/server/grpc/chat/v2/server.go | 11 ++++++----- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index dac3ff65..2b538843 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.7-0.20240619134635-bde7104d4467 + github.com/code-payments/code-protobuf-api v1.16.7-0.20240620175619-a8c06e297df2 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index 1ad4c9a3..625a2ae8 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240619134635-bde7104d4467 h1:BQgr91ASdCdxs8loz54TzmZQ0e6gDk1T7pkQxChJpdg= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240619134635-bde7104d4467/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240620175619-a8c06e297df2 h1:0XEbcqkWk9AK/wOW53C7lAlVGSwrwhMgEAtC2DGWv3Y= +github.com/code-payments/code-protobuf-api v1.16.7-0.20240620175619-a8c06e297df2/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= diff --git a/pkg/code/server/grpc/chat/v2/server.go b/pkg/code/server/grpc/chat/v2/server.go index 09abdabc..bc5351a2 100644 --- a/pkg/code/server/grpc/chat/v2/server.go +++ b/pkg/code/server/grpc/chat/v2/server.go @@ -505,7 +505,7 @@ func (s *server) flushPointers(ctx context.Context, chatId chat.ChatId, stream * event := &chatpb.ChatStreamEvent{ Type: &chatpb.ChatStreamEvent_Pointer{ Pointer: &chatpb.Pointer{ - Kind: optionalPointer.kind.ToProto(), + Type: optionalPointer.kind.ToProto(), Value: optionalPointer.value.ToProto(), MemberId: memberRecord.MemberId.ToProto(), }, @@ -859,7 +859,7 @@ func (s *server) AdvancePointer(ctx context.Context, req *chatpb.AdvancePointerR } log = log.WithField("member_id", memberId.String()) - pointerType := chat.GetPointerTypeFromProto(req.Pointer.Kind) + pointerType := chat.GetPointerTypeFromProto(req.Pointer.Type) log = log.WithField("pointer_type", pointerType.String()) switch pointerType { case chat.PointerTypeDelivered, chat.PointerTypeRead: @@ -1079,7 +1079,8 @@ func (s *server) RevealIdentity(ctx context.Context, req *chatpb.RevealIdentityR switch err { case nil: return &chatpb.RevealIdentityResponse{ - Result: chatpb.RevealIdentityResponse_OK, + Result: chatpb.RevealIdentityResponse_OK, + Message: chatMessage, }, nil case chat.ErrMemberIdentityAlreadyUpgraded: return &chatpb.RevealIdentityResponse{ @@ -1224,7 +1225,7 @@ func (s *server) SetSubscriptionState(ctx context.Context, req *chatpb.SetSubscr func (s *server) toProtoChat(ctx context.Context, chatRecord *chat.ChatRecord, memberRecords []*chat.MemberRecord, myIdentitiesByPlatform map[chat.Platform]string) (*chatpb.ChatMetadata, error) { protoChat := &chatpb.ChatMetadata{ ChatId: chatRecord.ChatId.ToProto(), - Kind: chatRecord.ChatType.ToProto(), + Type: chatRecord.ChatType.ToProto(), Cursor: &chatpb.Cursor{Value: query.ToCursor(uint64(chatRecord.Id))}, } @@ -1270,7 +1271,7 @@ func (s *server) toProtoChat(ctx context.Context, chatRecord *chat.ChatRecord, m } pointers = append(pointers, &chatpb.Pointer{ - Kind: optionalPointer.kind.ToProto(), + Type: optionalPointer.kind.ToProto(), Value: optionalPointer.value.ToProto(), MemberId: memberRecord.MemberId.ToProto(), }) From 54aa95fc44a2c5284de8c31a03ccc164cd6a8f1a Mon Sep 17 00:00:00 2001 From: Jeff Yanta Date: Thu, 20 Jun 2024 15:56:55 -0400 Subject: [PATCH 40/40] Pull official proto API release with v2 chat service --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 2b538843..4df1f59d 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( firebase.google.com/go/v4 v4.8.0 github.com/aws/aws-sdk-go-v2 v0.17.0 github.com/bits-and-blooms/bloom/v3 v3.1.0 - github.com/code-payments/code-protobuf-api v1.16.7-0.20240620175619-a8c06e297df2 + github.com/code-payments/code-protobuf-api v1.17.0 github.com/emirpasic/gods v1.12.0 github.com/envoyproxy/protoc-gen-validate v1.0.4 github.com/golang-jwt/jwt/v5 v5.0.0 diff --git a/go.sum b/go.sum index 625a2ae8..90a0e4b7 100644 --- a/go.sum +++ b/go.sum @@ -121,8 +121,8 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240620175619-a8c06e297df2 h1:0XEbcqkWk9AK/wOW53C7lAlVGSwrwhMgEAtC2DGWv3Y= -github.com/code-payments/code-protobuf-api v1.16.7-0.20240620175619-a8c06e297df2/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= +github.com/code-payments/code-protobuf-api v1.17.0 h1:zqLrhm54purzsKYb+5CZ3fJZCIVzmuZ31yeARUkuyWE= +github.com/code-payments/code-protobuf-api v1.17.0/go.mod h1:pHQm75vydD6Cm2qHAzlimW6drysm489Z4tVxC2zHSsU= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6 h1:NmTXa/uVnDyp0TY5MKi197+3HWcnYWfnHGyaFthlnGw= github.com/containerd/continuity v0.0.0-20190827140505-75bee3e2ccb6/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=