Skip to content

Commit 1e86d48

Browse files
author
jeffyanta
authored
Merge pull request #19 from code-payments/activity-feed
activity: initial activity feed implementation
2 parents 781f488 + 735951c commit 1e86d48

File tree

35 files changed

+1575
-65
lines changed

35 files changed

+1575
-65
lines changed

activity/localization.go

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package activity
2+
3+
import (
4+
"context"
5+
"errors"
6+
7+
"golang.org/x/text/language"
8+
"golang.org/x/text/message"
9+
10+
activitypb "github.com/code-payments/flipchat-protobuf-api/generated/go/activity/v1"
11+
12+
codekin "github.com/code-payments/code-server/pkg/kin"
13+
)
14+
15+
var (
16+
kinAmountPrinter = message.NewPrinter(language.English)
17+
)
18+
19+
func InjectLocalizedText(ctx context.Context, notification *activitypb.Notification) error {
20+
var localizedText string
21+
switch typed := notification.AdditionalMetadata.(type) {
22+
case *activitypb.Notification_WelcomeBonus:
23+
localizedText = kinAmountPrinter.Sprintf("You received ⬢\u00A0%d\u00A0Kin welcome bonus", codekin.FromQuarks(typed.WelcomeBonus.QuarksReceived))
24+
case *activitypb.Notification_WeeklyBonus:
25+
localizedText = kinAmountPrinter.Sprintf("You received ⬢\u00A0%d\u00A0Kin weekly bonus", codekin.FromQuarks(typed.WeeklyBonus.QuarksReceived))
26+
case *activitypb.Notification_CreateGroup:
27+
localizedText = kinAmountPrinter.Sprintf("You paid ⬢\u00A0%d\u00A0Kin to create a new Flipchat", codekin.FromQuarks(typed.CreateGroup.QuarksSpent))
28+
case *activitypb.Notification_SendListenerMessage:
29+
localizedText = kinAmountPrinter.Sprintf("You paid ⬢\u00A0%d\u00A0Kin", codekin.FromQuarks(typed.SendListenerMessage.QuarksSpent))
30+
case *activitypb.Notification_SendTip:
31+
localizedText = kinAmountPrinter.Sprintf("You tipped ⬢\u00A0%d\u00A0Kin", codekin.FromQuarks(typed.SendTip.TotalQuarksSent))
32+
case *activitypb.Notification_ReceivedTip:
33+
localizedText = kinAmountPrinter.Sprintf("You received ⬢\u00A0%d\u00A0Kin", codekin.FromQuarks(typed.ReceivedTip.TotalQuarksReceived))
34+
/*
35+
case *activitypb.Notification_PromotedToSpeaker:
36+
profile, err := profiles.GetProfile(ctx, typed.PromotedToSpeaker.PromtedBy)
37+
if err != nil {
38+
return err
39+
}
40+
41+
chatMd, err := chats.GetChatMetadata(ctx, typed.PromotedToSpeaker.ChatId)
42+
if err != nil {
43+
return err
44+
}
45+
46+
if len(profile.DisplayName) == 0 {
47+
return errors.New("user doesn't have a display name")
48+
}
49+
if len(chatMd.DisplayName) == 0 {
50+
return errors.New("chat doesn't have a display name")
51+
}
52+
53+
localizedText = kinAmountPrinter.Sprintf("%s made you a speaker in %s", profile.DisplayName, chatMd.DisplayName)
54+
*/
55+
default:
56+
return errors.New("unsupported notification type")
57+
}
58+
notification.LocalizedText = localizedText
59+
return nil
60+
}

activity/memory/server_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package memory
2+
3+
import (
4+
"testing"
5+
6+
account "github.com/code-payments/flipchat-server/account/memory"
7+
"github.com/code-payments/flipchat-server/activity/tests"
8+
)
9+
10+
func TestActivity_MemoryServer(t *testing.T) {
11+
testStore := NewInMemory()
12+
accounts := account.NewInMemory()
13+
teardown := func() {
14+
testStore.(*InMemoryStore).reset()
15+
}
16+
tests.RunServerTests(t, accounts, testStore, teardown)
17+
}

activity/memory/store.go

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package memory
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"sort"
7+
"sync"
8+
9+
activitypb "github.com/code-payments/flipchat-protobuf-api/generated/go/activity/v1"
10+
commonpb "github.com/code-payments/flipchat-protobuf-api/generated/go/common/v1"
11+
"google.golang.org/protobuf/proto"
12+
13+
"github.com/code-payments/flipchat-server/activity"
14+
"github.com/code-payments/flipchat-server/protoutil"
15+
)
16+
17+
type NotificationsByTimestamp []*activitypb.Notification
18+
19+
func (a NotificationsByTimestamp) Len() int { return len(a) }
20+
func (a NotificationsByTimestamp) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
21+
func (a NotificationsByTimestamp) Less(i, j int) bool {
22+
return a[i].Ts.AsTime().Before(a[j].Ts.AsTime())
23+
}
24+
25+
type InMemoryStore struct {
26+
mu sync.RWMutex
27+
notifications map[string][]*activitypb.Notification
28+
}
29+
30+
func NewInMemory() activity.Store {
31+
return &InMemoryStore{
32+
notifications: map[string][]*activitypb.Notification{},
33+
}
34+
}
35+
36+
func (m *InMemoryStore) SaveNotification(ctx context.Context, activityFeedType activitypb.ActivityFeedType, userID *commonpb.UserId, notification *activitypb.Notification) (*activitypb.Notification, error) {
37+
if activityFeedType != activitypb.ActivityFeedType_TRANSACTION_HISTORY {
38+
return nil, activity.ErrInvalidActivityFeedType
39+
}
40+
41+
m.mu.Lock()
42+
defer m.mu.Unlock()
43+
44+
var existing *activitypb.Notification
45+
for _, n := range m.notifications[string(userID.Value)] {
46+
if bytes.Equal(notification.Id.Value, n.Id.Value) {
47+
existing = n
48+
break
49+
}
50+
}
51+
52+
switch typed := notification.AdditionalMetadata.(type) {
53+
case
54+
*activitypb.Notification_WelcomeBonus,
55+
*activitypb.Notification_WeeklyBonus,
56+
*activitypb.Notification_CreateGroup,
57+
*activitypb.Notification_SendListenerMessage:
58+
case *activitypb.Notification_SendTip:
59+
if existing != nil {
60+
existing.GetSendTip().TotalQuarksSent += typed.SendTip.TotalQuarksSent
61+
}
62+
case *activitypb.Notification_ReceivedTip:
63+
if existing != nil {
64+
existing.GetReceivedTip().TotalQuarksReceived += typed.ReceivedTip.TotalQuarksReceived
65+
}
66+
default:
67+
return nil, activity.ErrInvalidNotificationType
68+
}
69+
70+
if existing == nil {
71+
existing = proto.Clone(notification).(*activitypb.Notification)
72+
m.notifications[string(userID.Value)] = append(m.notifications[string(userID.Value)], proto.Clone(notification).(*activitypb.Notification))
73+
}
74+
return proto.Clone(existing).(*activitypb.Notification), nil
75+
}
76+
77+
func (m *InMemoryStore) GetLatestNotifications(ctx context.Context, activityFeedType activitypb.ActivityFeedType, userID *commonpb.UserId, limit int) ([]*activitypb.Notification, error) {
78+
if activityFeedType != activitypb.ActivityFeedType_TRANSACTION_HISTORY {
79+
return nil, activity.ErrInvalidActivityFeedType
80+
}
81+
82+
m.mu.RLock()
83+
defer m.mu.RUnlock()
84+
85+
res := protoutil.SliceClone(m.notifications[string(userID.Value)])
86+
87+
sorted := NotificationsByTimestamp(res)
88+
sort.Sort(sort.Reverse(sorted))
89+
90+
if len(sorted) > limit {
91+
sorted = sorted[:limit]
92+
}
93+
94+
return sorted, nil
95+
}
96+
97+
func (m *InMemoryStore) reset() {
98+
m.mu.Lock()
99+
m.notifications = map[string][]*activitypb.Notification{}
100+
m.mu.Unlock()
101+
}

activity/memory/store_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package memory
2+
3+
import (
4+
"testing"
5+
6+
"github.com/code-payments/flipchat-server/activity/tests"
7+
)
8+
9+
func TestActivity_MemoryStore(t *testing.T) {
10+
testStore := NewInMemory()
11+
teardown := func() {
12+
testStore.(*InMemoryStore).reset()
13+
}
14+
tests.RunStoreTests(t, testStore, teardown)
15+
}

activity/model.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package activity
2+
3+
import (
4+
"crypto/sha256"
5+
"encoding/binary"
6+
"encoding/hex"
7+
"errors"
8+
9+
activitypb "github.com/code-payments/flipchat-protobuf-api/generated/go/activity/v1"
10+
commonpb "github.com/code-payments/flipchat-protobuf-api/generated/go/common/v1"
11+
)
12+
13+
type NotificationType uint32
14+
15+
const (
16+
NotificationTypeUnknown = iota
17+
NotificationTypeWelcomeBonus
18+
NotificationTypeWeeklyBonus
19+
NotificationTypeCreateGroup
20+
NotificationTypeSendListenerMessage
21+
NotificationTypeSendTip
22+
NotificationTypeReceivedTip
23+
NotificationTypePromotedToSpeaker
24+
)
25+
26+
func NotificationIDString(id *activitypb.NotificationId) string {
27+
if id == nil {
28+
return "<invalid>"
29+
}
30+
return hex.EncodeToString(id.Value)
31+
}
32+
33+
func GetNotificationID(notificationType NotificationType, userID *commonpb.UserId, additionalSeeds ...[]byte) (*activitypb.NotificationId, error) {
34+
if notificationType == NotificationTypeUnknown {
35+
return nil, errors.New("notification type cannot be unknown")
36+
}
37+
38+
var notificationTypeBytes [4]byte
39+
binary.LittleEndian.PutUint32(notificationTypeBytes[:], uint32(notificationType))
40+
41+
hasher := sha256.New()
42+
_, err := hasher.Write(notificationTypeBytes[:])
43+
if err != nil {
44+
return nil, err
45+
}
46+
_, err = hasher.Write(userID.Value)
47+
if err != nil {
48+
return nil, err
49+
}
50+
for _, seed := range additionalSeeds {
51+
_, err = hasher.Write(seed)
52+
if err != nil {
53+
return nil, err
54+
}
55+
}
56+
hashed := hasher.Sum(nil)
57+
58+
return &activitypb.NotificationId{Value: hashed}, nil
59+
}

0 commit comments

Comments
 (0)