Skip to content

Commit 9256fdc

Browse files
committed
chats: add (temporary) plumbing for tip notifications.
Adds enough plumbing for the tip notifications to flow through v2 (without breaking the previous system, hopefully). When the protocol is more flushed out, and the base messaging infra is shored up a bit, this will likely be revisted
1 parent e29881f commit 9256fdc

File tree

16 files changed

+357
-29
lines changed

16 files changed

+357
-29
lines changed

pkg/code/async/geyser/messenger.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ func processPotentialBlockchainMessage(ctx context.Context, data code_data.Provi
165165
return errors.Wrap(err, "error creating proto message")
166166
}
167167

168-
canPush, err := chat_util.SendChatMessage(
168+
canPush, err := chat_util.SendNotificationChatMessageV1(
169169
ctx,
170170
data,
171171
asciiBaseDomain,

pkg/code/chat/message_cash_transactions.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func SendCashTransactionsExchangeMessage(ctx context.Context, data code_data.Pro
148148
return errors.Wrap(err, "error creating proto chat message")
149149
}
150150

151-
_, err = SendChatMessage(
151+
_, err = SendNotificationChatMessageV1(
152152
ctx,
153153
data,
154154
CashTransactionsName,

pkg/code/chat/message_code_team.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import (
1616

1717
// SendCodeTeamMessage sends a message to the Code Team chat.
1818
func SendCodeTeamMessage(ctx context.Context, data code_data.Provider, receiver *common.Account, chatMessage *chatpb.ChatMessage) (bool, error) {
19-
return SendChatMessage(
19+
return SendNotificationChatMessageV1(
2020
ctx,
2121
data,
2222
CodeTeamName,

pkg/code/chat/message_kin_purchases.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ func GetKinPurchasesChatId(owner *common.Account) chat_v1.ChatId {
2323

2424
// SendKinPurchasesMessage sends a message to the Kin Purchases chat.
2525
func SendKinPurchasesMessage(ctx context.Context, data code_data.Provider, receiver *common.Account, chatMessage *chatpb.ChatMessage) (bool, error) {
26-
return SendChatMessage(
26+
return SendNotificationChatMessageV1(
2727
ctx,
2828
data,
2929
KinPurchasesName,

pkg/code/chat/message_merchant.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ func SendMerchantExchangeMessage(ctx context.Context, data code_data.Provider, i
161161
return nil, errors.Wrap(err, "error creating proto chat message")
162162
}
163163

164-
canPush, err := SendChatMessage(
164+
canPush, err := SendNotificationChatMessageV1(
165165
ctx,
166166
data,
167167
chatTitle,

pkg/code/chat/message_tips.go

Lines changed: 58 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,23 +2,36 @@ package chat
22

33
import (
44
"context"
5+
"fmt"
56

7+
"github.com/mr-tron/base58"
68
"github.com/pkg/errors"
9+
"github.com/sirupsen/logrus"
10+
"google.golang.org/protobuf/types/known/timestamppb"
711

812
chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1"
13+
chatv2pb "github.com/code-payments/code-protobuf-api/generated/go/chat/v2"
14+
commonpb "github.com/code-payments/code-protobuf-api/generated/go/common/v1"
915

1016
"github.com/code-payments/code-server/pkg/code/common"
1117
code_data "github.com/code-payments/code-server/pkg/code/data"
1218
chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1"
19+
chat_v2 "github.com/code-payments/code-server/pkg/code/data/chat/v2"
1320
"github.com/code-payments/code-server/pkg/code/data/intent"
21+
chat_server "github.com/code-payments/code-server/pkg/code/server/grpc/chat/v2"
1422
)
1523

1624
// SendTipsExchangeMessage sends a message to the Tips chat with exchange data
1725
// content related to the submitted intent. Intents that don't belong in the
1826
// Tips chat will be ignored.
1927
//
2028
// Note: Tests covered in SubmitIntent history tests
21-
func SendTipsExchangeMessage(ctx context.Context, data code_data.Provider, intentRecord *intent.Record) ([]*MessageWithOwner, error) {
29+
func SendTipsExchangeMessage(ctx context.Context, data code_data.Provider, notifier chat_server.Notifier, intentRecord *intent.Record) ([]*MessageWithOwner, error) {
30+
intentIdRaw, err := base58.Decode(intentRecord.IntentId)
31+
if err != nil {
32+
return nil, fmt.Errorf("invalid intent id: %w", err)
33+
}
34+
2235
messageId := intentRecord.IntentId
2336

2437
exchangeData, ok := getExchangeDataFromIntent(intentRecord)
@@ -30,7 +43,6 @@ func SendTipsExchangeMessage(ctx context.Context, data code_data.Provider, inten
3043
switch intentRecord.IntentType {
3144
case intent.SendPrivatePayment:
3245
if !intentRecord.SendPrivatePaymentMetadata.IsTip {
33-
// Not a tip
3446
return nil, nil
3547
}
3648

@@ -61,30 +73,68 @@ func SendTipsExchangeMessage(ctx context.Context, data code_data.Provider, inten
6173
},
6274
},
6375
}
64-
protoMessage, err := newProtoChatMessage(messageId, content, intentRecord.CreatedAt)
76+
77+
v1Message, err := newProtoChatMessage(messageId, content, intentRecord.CreatedAt)
6578
if err != nil {
6679
return nil, errors.Wrap(err, "error creating proto chat message")
6780
}
6881

69-
canPush, err := SendChatMessage(
82+
v2Message := &chatv2pb.ChatMessage{
83+
MessageId: chat_v2.GenerateMessageId().ToProto(),
84+
Content: []*chatv2pb.Content{
85+
{
86+
Type: &chatv2pb.Content_ExchangeData{
87+
ExchangeData: &chatv2pb.ExchangeDataContent{
88+
Verb: chatv2pb.ExchangeDataContent_Verb(verb),
89+
ExchangeData: &chatv2pb.ExchangeDataContent_Exact{
90+
Exact: exchangeData,
91+
},
92+
Reference: &chatv2pb.ExchangeDataContent_Intent{
93+
Intent: &commonpb.IntentId{Value: intentIdRaw},
94+
},
95+
},
96+
},
97+
},
98+
},
99+
Ts: timestamppb.New(intentRecord.CreatedAt),
100+
}
101+
102+
canPush, err := SendNotificationChatMessageV1(
70103
ctx,
71104
data,
72105
TipsName,
73106
chat_v1.ChatTypeInternal,
74107
true,
75108
receiver,
76-
protoMessage,
109+
v1Message,
77110
verb != chatpb.ExchangeDataContent_RECEIVED_TIP,
78111
)
79-
if err != nil && err != chat_v1.ErrMessageAlreadyExists {
80-
return nil, errors.Wrap(err, "error persisting chat message")
112+
if err != nil && !errors.Is(err, chat_v1.ErrMessageAlreadyExists) {
113+
return nil, errors.Wrap(err, "error persisting v1 chat message")
114+
}
115+
116+
_, err = SendNotificationChatMessageV2(
117+
ctx,
118+
data,
119+
notifier,
120+
TipsName,
121+
true,
122+
receiver,
123+
v2Message,
124+
intentRecord.IntentId,
125+
verb != chatpb.ExchangeDataContent_RECEIVED_TIP,
126+
)
127+
if err != nil {
128+
// TODO: Eventually we'll want to return an error, but for now we'll log
129+
// since we're not in 'prod' yet.
130+
logrus.StandardLogger().WithError(err).Warn("Failed to send notification message (v2)")
81131
}
82132

83133
if canPush {
84134
messagesToPush = append(messagesToPush, &MessageWithOwner{
85135
Owner: receiver,
86136
Title: TipsName,
87-
Message: protoMessage,
137+
Message: v1Message,
88138
})
89139
}
90140
}

pkg/code/chat/sender.go

Lines changed: 137 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,30 @@ package chat
22

33
import (
44
"context"
5+
"database/sql"
56
"errors"
7+
"fmt"
68
"time"
79

810
"github.com/mr-tron/base58"
911
"google.golang.org/protobuf/proto"
1012

1113
chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1"
14+
chatv2pb "github.com/code-payments/code-protobuf-api/generated/go/chat/v2"
1215

1316
"github.com/code-payments/code-server/pkg/code/common"
1417
code_data "github.com/code-payments/code-server/pkg/code/data"
1518
chat_v1 "github.com/code-payments/code-server/pkg/code/data/chat/v1"
19+
chat_v2 "github.com/code-payments/code-server/pkg/code/data/chat/v2"
20+
chatserver "github.com/code-payments/code-server/pkg/code/server/grpc/chat/v2"
1621
)
1722

18-
// SendChatMessage sends a chat message to a receiving owner account.
23+
// SendNotificationChatMessageV1 sends a chat message to a receiving owner account.
1924
//
2025
// Note: This function is not responsible for push notifications. This method
2126
// might be called within the context of a DB transaction, which might have
22-
// unrelated failures. A hint as to whether a push should be sent is provided.
23-
func SendChatMessage(
27+
// unrelated failures. A hint whether a push should be sent is provided.
28+
func SendNotificationChatMessageV1(
2429
ctx context.Context,
2530
data code_data.Provider,
2631
chatTitle string,
@@ -80,7 +85,7 @@ func SendChatMessage(
8085
}
8186

8287
err = data.PutChatV1(ctx, chatRecord)
83-
if err != nil && err != chat_v1.ErrChatAlreadyExists {
88+
if err != nil && !errors.Is(err, chat_v1.ErrChatAlreadyExists) {
8489
return false, err
8590
}
8691
default:
@@ -115,3 +120,131 @@ func SendChatMessage(
115120

116121
return canPushMessage, nil
117122
}
123+
124+
func SendNotificationChatMessageV2(
125+
ctx context.Context,
126+
data code_data.Provider,
127+
notifier chatserver.Notifier,
128+
chatTitle string,
129+
isVerifiedChat bool,
130+
receiver *common.Account,
131+
protoMessage *chatv2pb.ChatMessage,
132+
intentId string,
133+
isSilentMessage bool,
134+
) (canPushMessage bool, err error) {
135+
chatId := chat_v2.GetChatId(chatTitle, receiver.PublicKey().ToBase58(), isVerifiedChat)
136+
137+
if protoMessage.Cursor != nil {
138+
// Let the utilities and GetMessages RPC handle cursors
139+
return false, errors.New("cursor must not be set")
140+
}
141+
142+
if err := protoMessage.Validate(); err != nil {
143+
return false, err
144+
}
145+
146+
messageId, err := chat_v2.GetMessageIdFromProto(protoMessage.MessageId)
147+
if err != nil {
148+
return false, fmt.Errorf("invalid message id: %w", err)
149+
}
150+
151+
// Clear out extracted metadata as a space optimization
152+
cloned := proto.Clone(protoMessage).(*chatv2pb.ChatMessage)
153+
cloned.MessageId = nil
154+
cloned.Ts = nil
155+
cloned.Cursor = nil
156+
157+
marshalled, err := proto.Marshal(cloned)
158+
if err != nil {
159+
return false, err
160+
}
161+
162+
canPersistMessage := true
163+
canPushMessage = !isSilentMessage
164+
165+
//
166+
// Step 1: Check to see if we need to create the chat.
167+
//
168+
_, err = data.GetChatByIdV2(ctx, chatId)
169+
if errors.Is(err, chat_v2.ErrChatNotFound) {
170+
chatRecord := &chat_v2.ChatRecord{
171+
ChatId: chatId,
172+
ChatType: chat_v2.ChatTypeNotification,
173+
ChatTitle: &chatTitle,
174+
IsVerified: isVerifiedChat,
175+
176+
CreatedAt: time.Now(),
177+
}
178+
179+
err = data.ExecuteInTx(ctx, sql.LevelDefault, func(ctx context.Context) error {
180+
err = data.PutChatV2(ctx, chatRecord)
181+
if err != nil && !errors.Is(err, chat_v2.ErrChatExists) {
182+
return fmt.Errorf("failed to initialize chat: %w", err)
183+
}
184+
185+
err = data.PutChatMemberV2(ctx, &chat_v2.MemberRecord{
186+
ChatId: chatId,
187+
MemberId: chat_v2.GenerateMemberId(),
188+
Platform: chat_v2.PlatformCode,
189+
PlatformId: receiver.PublicKey().ToBase58(),
190+
JoinedAt: time.Now(),
191+
})
192+
if err != nil {
193+
return fmt.Errorf("failed to initialize chat with member: %w", err)
194+
}
195+
196+
return nil
197+
})
198+
if err != nil {
199+
return false, err
200+
}
201+
} else if err != nil {
202+
return false, err
203+
}
204+
205+
//
206+
// Step 2: Ensure that there is exactly 1 member in the chat.
207+
//
208+
members, err := data.GetAllChatMembersV2(ctx, chatId)
209+
if errors.Is(err, chat_v2.ErrMemberNotFound) { // TODO: This is a weird error...
210+
return false, nil
211+
} else if err != nil {
212+
return false, err
213+
}
214+
if len(members) > 1 {
215+
// TODO: This _could_ get weird if client or someone else decides to join as another member.
216+
return false, errors.New("notification chat should have at most 1 member")
217+
}
218+
219+
canPersistMessage = !members[0].IsUnsubscribed
220+
canPushMessage = canPushMessage && canPersistMessage && !members[0].IsMuted
221+
222+
if canPersistMessage {
223+
refType := chat_v2.ReferenceTypeIntent
224+
messageRecord := &chat_v2.MessageRecord{
225+
ChatId: chatId,
226+
MessageId: messageId,
227+
228+
Data: marshalled,
229+
IsSilent: isSilentMessage,
230+
231+
ReferenceType: &refType,
232+
Reference: &intentId,
233+
}
234+
235+
// TODO: Once we have a better idea on the data modeling around chatv2,
236+
// we may wish to have the server manage the creation of messages
237+
// (and chats?) as well. That would also put the
238+
err = data.PutChatMessageV2(ctx, messageRecord)
239+
if err != nil {
240+
return false, err
241+
}
242+
243+
notifier.NotifyMessage(ctx, chatId, protoMessage)
244+
}
245+
246+
// TODO: Once we move more things over to chatv2, we will need to increment
247+
// badge count here. We don't currently, as it would result in a double
248+
// push.
249+
return canPushMessage, nil
250+
}

pkg/code/chat/sender_test.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,8 @@ func TestSendChatMessage_HappyPath(t *testing.T) {
3333
chatMessage := newRandomChatMessage(t, i+1)
3434
expectedBadgeCount += 1
3535

36-
canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false)
36+
canPush, err := SendNotificationChatMessageV1(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false)
3737
require.NoError(t, err)
38-
3938
assert.True(t, canPush)
4039

4140
assert.NotNil(t, chatMessage.MessageId)
@@ -56,7 +55,7 @@ func TestSendChatMessage_VerifiedChat(t *testing.T) {
5655

5756
for _, isVerified := range []bool{true, false} {
5857
chatMessage := newRandomChatMessage(t, 1)
59-
_, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, isVerified, receiver, chatMessage, true)
58+
_, err := SendNotificationChatMessageV1(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, isVerified, receiver, chatMessage, true)
6059
require.NoError(t, err)
6160
env.assertChatRecordSaved(t, chatTitle, receiver, isVerified)
6261
}
@@ -71,7 +70,7 @@ func TestSendChatMessage_SilentMessage(t *testing.T) {
7170

7271
for i, isSilent := range []bool{true, false} {
7372
chatMessage := newRandomChatMessage(t, 1)
74-
canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, isSilent)
73+
canPush, err := SendNotificationChatMessageV1(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, isSilent)
7574
require.NoError(t, err)
7675
assert.Equal(t, !isSilent, canPush)
7776
env.assertChatMessageRecordSaved(t, chatId, chatMessage, isSilent)
@@ -92,7 +91,7 @@ func TestSendChatMessage_MuteState(t *testing.T) {
9291
}
9392

9493
chatMessage := newRandomChatMessage(t, 1)
95-
canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false)
94+
canPush, err := SendNotificationChatMessageV1(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false)
9695
require.NoError(t, err)
9796
assert.Equal(t, !isMuted, canPush)
9897
env.assertChatMessageRecordSaved(t, chatId, chatMessage, false)
@@ -113,7 +112,7 @@ func TestSendChatMessage_SubscriptionState(t *testing.T) {
113112
}
114113

115114
chatMessage := newRandomChatMessage(t, 1)
116-
canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false)
115+
canPush, err := SendNotificationChatMessageV1(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false)
117116
require.NoError(t, err)
118117
assert.Equal(t, !isUnsubscribed, canPush)
119118
if isUnsubscribed {
@@ -135,7 +134,7 @@ func TestSendChatMessage_InvalidProtoMessage(t *testing.T) {
135134
chatMessage := newRandomChatMessage(t, 1)
136135
chatMessage.Content = nil
137136

138-
canPush, err := SendChatMessage(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false)
137+
canPush, err := SendNotificationChatMessageV1(env.ctx, env.data, chatTitle, chat_v1.ChatTypeInternal, true, receiver, chatMessage, false)
139138
assert.Error(t, err)
140139
assert.False(t, canPush)
141140
env.assertChatRecordNotSaved(t, chatId)

0 commit comments

Comments
 (0)