feat: sync ephemeral transient messages
Sync telesrv 570ccf8 (feat(ephemeral): implement Layer 228 transient messages). Skipped telesrv docs changes per public sync rules; normalized the public appearance seed label.
This commit is contained in:
parent
3f78eaa2c6
commit
f49c817def
53 changed files with 5793 additions and 112 deletions
220
internal/rpc/ephemeral_push_test.go
Normal file
220
internal/rpc/ephemeral_push_test.go
Normal file
|
|
@ -0,0 +1,220 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"github.com/iamxvbaba/td/proto"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store"
|
||||
)
|
||||
|
||||
type ephemeralPushChannels struct {
|
||||
ChannelsService
|
||||
view domain.ChannelView
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *ephemeralPushChannels) ResolveChannel(context.Context, int64, int64) (domain.ChannelView, error) {
|
||||
s.calls++
|
||||
return s.view, nil
|
||||
}
|
||||
|
||||
type ephemeralPushSessions struct {
|
||||
SessionBinder
|
||||
OnlineUserProvider
|
||||
mu sync.Mutex
|
||||
online bool
|
||||
broadcasts []ephemeralPushCapture
|
||||
targeted []ephemeralPushCapture
|
||||
}
|
||||
|
||||
type ephemeralPushCapture struct {
|
||||
userID int64
|
||||
authKey [8]byte
|
||||
minLayer int
|
||||
message tg.UpdatesClass
|
||||
}
|
||||
|
||||
func (s *ephemeralPushSessions) IsUserOnline(int64) bool { return s.online }
|
||||
|
||||
func (s *ephemeralPushSessions) PushToUserTransientAtLeastLayer(_ context.Context, userID int64, minLayer int, _ proto.MessageType, message tg.UpdatesClass, _ time.Duration) (int, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.broadcasts = append(s.broadcasts, ephemeralPushCapture{userID: userID, minLayer: minLayer, message: message})
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (s *ephemeralPushSessions) PushToUserAuthKeyTransientAtLeastLayer(_ context.Context, userID int64, authKey [8]byte, minLayer int, _ proto.MessageType, message tg.UpdatesClass, _ time.Duration) (int, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.targeted = append(s.targeted, ephemeralPushCapture{userID: userID, authKey: authKey, minLayer: minLayer, message: message})
|
||||
return 1, nil
|
||||
}
|
||||
|
||||
func (s *ephemeralPushSessions) counts() (int, int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
return len(s.broadcasts), len(s.targeted)
|
||||
}
|
||||
|
||||
type inMemoryEphemeralBroker struct {
|
||||
mu sync.Mutex
|
||||
subscribers []func(context.Context, store.EphemeralPush)
|
||||
registered chan struct{}
|
||||
published []store.EphemeralPush
|
||||
}
|
||||
|
||||
func newInMemoryEphemeralBroker() *inMemoryEphemeralBroker {
|
||||
return &inMemoryEphemeralBroker{registered: make(chan struct{}, 8)}
|
||||
}
|
||||
|
||||
func (b *inMemoryEphemeralBroker) PublishEphemeralPush(ctx context.Context, event store.EphemeralPush) error {
|
||||
b.mu.Lock()
|
||||
b.published = append(b.published, event)
|
||||
handlers := append([]func(context.Context, store.EphemeralPush){}, b.subscribers...)
|
||||
b.mu.Unlock()
|
||||
for _, handler := range handlers {
|
||||
handler(ctx, event)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *inMemoryEphemeralBroker) SubscribeEphemeralPushes(ctx context.Context, handler func(context.Context, store.EphemeralPush)) error {
|
||||
b.mu.Lock()
|
||||
b.subscribers = append(b.subscribers, handler)
|
||||
b.mu.Unlock()
|
||||
b.registered <- struct{}{}
|
||||
<-ctx.Done()
|
||||
return ctx.Err()
|
||||
}
|
||||
|
||||
func TestEphemeralPushMultiInstanceSourceDedupAndLayerRouting(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
broker := newInMemoryEphemeralBroker()
|
||||
users := mapUsersService{users: map[int64]domain.User{
|
||||
1001: {ID: 1001, FirstName: "Bot", Bot: true},
|
||||
2001: {ID: 2001, FirstName: "Alice"},
|
||||
}}
|
||||
view := domain.ChannelView{
|
||||
Channel: domain.Channel{ID: 3001, AccessHash: 7, Title: "Group", Megagroup: true},
|
||||
Self: domain.ChannelMember{ChannelID: 3001, UserID: 2001, Status: domain.ChannelMemberActive},
|
||||
}
|
||||
channels1, channels2 := &ephemeralPushChannels{view: view}, &ephemeralPushChannels{view: view}
|
||||
sessions1, sessions2 := &ephemeralPushSessions{online: true}, &ephemeralPushSessions{online: true}
|
||||
r1 := New(Config{InstanceID: "one"}, Deps{Users: users, Channels: channels1, Sessions: sessions1, EphemeralPush: broker}, zaptest.NewLogger(t), clock.System)
|
||||
r2 := New(Config{InstanceID: "two"}, Deps{Users: users, Channels: channels2, Sessions: sessions2, EphemeralPush: broker}, zaptest.NewLogger(t), clock.System)
|
||||
go r1.RunEphemeralPushSubscriber(ctx)
|
||||
go r2.RunEphemeralPushSubscriber(ctx)
|
||||
for range 2 {
|
||||
select {
|
||||
case <-broker.registered:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("subscriber did not register")
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
message := domain.EphemeralMessage{
|
||||
ID: 77, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 3001},
|
||||
SenderUserID: 1001, ReceiverUserID: 2001, Date: int(now.Unix()), RandomID: 78,
|
||||
Content: domain.EphemeralContent{Message: "private"}, PayloadHash: [32]byte{1}, Version: 1,
|
||||
CreatedAt: now, ExpiresAt: now.Add(domain.EphemeralMessageRetention),
|
||||
}
|
||||
r1.publishEphemeralPush(ctx, store.EphemeralPush{Kind: store.EphemeralPushNew, TargetUserID: 2001, Message: message})
|
||||
if broadcast, targeted := sessions1.counts(); broadcast != 1 || targeted != 0 {
|
||||
t.Fatalf("source delivery broadcast=%d targeted=%d", broadcast, targeted)
|
||||
}
|
||||
if broadcast, targeted := sessions2.counts(); broadcast != 1 || targeted != 0 {
|
||||
t.Fatalf("remote delivery broadcast=%d targeted=%d", broadcast, targeted)
|
||||
}
|
||||
if sessions1.broadcasts[0].minLayer != 228 || sessions2.broadcasts[0].minLayer != 228 {
|
||||
t.Fatalf("min layers source=%d remote=%d", sessions1.broadcasts[0].minLayer, sessions2.broadcasts[0].minLayer)
|
||||
}
|
||||
if len(broker.published) != 1 || broker.published[0].SourceID != "one" {
|
||||
t.Fatalf("published=%+v", broker.published)
|
||||
}
|
||||
|
||||
key := [8]byte{9, 8, 7}
|
||||
message.Deleted = true
|
||||
message.Version++
|
||||
message.Content = domain.EphemeralContent{}
|
||||
r2.deliverEphemeralPushLocal(ctx, store.EphemeralPush{
|
||||
Kind: store.EphemeralPushDelete, TargetUserID: 2001,
|
||||
TargetBusinessAuthKey: key, Message: message, Date: int(time.Now().Unix()),
|
||||
})
|
||||
_, targeted := sessions2.counts()
|
||||
if targeted != 1 || sessions2.targeted[0].authKey != key || sessions2.targeted[0].minLayer != 228 {
|
||||
t.Fatalf("targeted=%+v", sessions2.targeted)
|
||||
}
|
||||
deletedUpdates, ok := sessions2.targeted[0].message.(*tg.Updates)
|
||||
if !ok || deletedUpdates.Seq != 0 || len(deletedUpdates.Updates) != 1 {
|
||||
t.Fatalf("delete updates=%#v", sessions2.targeted[0].message)
|
||||
}
|
||||
deleted, ok := deletedUpdates.Updates[0].(*tg.UpdateDeleteEphemeralMessages)
|
||||
if !ok || len(deleted.IDs) != 1 || deleted.IDs[0] != message.ID {
|
||||
t.Fatalf("delete update=%#v", deletedUpdates.Updates[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestEphemeralMessageUpdatesAreTransientAndPtsFree(t *testing.T) {
|
||||
now := time.Now()
|
||||
router := New(Config{}, Deps{
|
||||
Users: mapUsersService{users: map[int64]domain.User{
|
||||
1001: {ID: 1001, FirstName: "Bot", Bot: true},
|
||||
2001: {ID: 2001, FirstName: "Alice"},
|
||||
}},
|
||||
Channels: &ephemeralPushChannels{view: domain.ChannelView{
|
||||
Channel: domain.Channel{ID: 3001, AccessHash: 7, Title: "Group", Megagroup: true},
|
||||
Self: domain.ChannelMember{ChannelID: 3001, UserID: 2001, Status: domain.ChannelMemberActive},
|
||||
}},
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
message := domain.EphemeralMessage{
|
||||
ID: 77, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 3001},
|
||||
SenderUserID: 1001, ReceiverUserID: 2001, Date: int(now.Unix()), RandomID: 78,
|
||||
Content: domain.EphemeralContent{Message: "private"}, PayloadHash: [32]byte{1}, Version: 1,
|
||||
CreatedAt: now, ExpiresAt: now.Add(domain.EphemeralMessageRetention),
|
||||
}
|
||||
updates, err := router.ephemeralMessageUpdates(context.Background(), 2001, message, false)
|
||||
if err != nil || updates.Seq != 0 || len(updates.Updates) != 1 {
|
||||
t.Fatalf("updates=%#v err=%v", updates, err)
|
||||
}
|
||||
if _, ok := updates.Updates[0].(*tg.UpdateNewEphemeralMessage); !ok {
|
||||
t.Fatalf("update type=%T", updates.Updates[0])
|
||||
}
|
||||
deleted := ephemeralDeleteUpdates(domain.EphemeralMessage{ID: message.ID, Peer: message.Peer}, int(now.Unix()))
|
||||
if deleted.Seq != 0 {
|
||||
t.Fatalf("delete seq=%d", deleted.Seq)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEphemeralPushOfflineSkipsHydration(t *testing.T) {
|
||||
channels := &ephemeralPushChannels{view: domain.ChannelView{Channel: domain.Channel{ID: 3001}}}
|
||||
sessions := &ephemeralPushSessions{online: false}
|
||||
now := time.Now()
|
||||
router := New(Config{InstanceID: "offline"}, Deps{
|
||||
Users: mapUsersService{users: map[int64]domain.User{}}, Channels: channels, Sessions: sessions,
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
router.deliverEphemeralPushLocal(context.Background(), store.EphemeralPush{
|
||||
Kind: store.EphemeralPushNew, TargetUserID: 2001,
|
||||
Message: domain.EphemeralMessage{
|
||||
ID: 77, Peer: domain.Peer{Type: domain.PeerTypeChannel, ID: 3001},
|
||||
SenderUserID: 1001, ReceiverUserID: 2001, Date: int(now.Unix()), RandomID: 78,
|
||||
Content: domain.EphemeralContent{Message: "private"}, PayloadHash: [32]byte{1}, Version: 1,
|
||||
CreatedAt: now, ExpiresAt: now.Add(domain.EphemeralMessageRetention),
|
||||
},
|
||||
})
|
||||
if channels.calls != 0 {
|
||||
t.Fatalf("offline push performed %d channel hydrations", channels.calls)
|
||||
}
|
||||
if broadcast, targeted := sessions.counts(); broadcast != 0 || targeted != 0 {
|
||||
t.Fatalf("offline delivery broadcast=%d targeted=%d", broadcast, targeted)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue