fix: sync protocol and discussion stability fixes
This commit is contained in:
parent
9f73dc20da
commit
aa21bd04e1
43 changed files with 7258 additions and 503 deletions
119
internal/store/memory/channel_linked_guest_authorization_test.go
Normal file
119
internal/store/memory/channel_linked_guest_authorization_test.go
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"telesrv/internal/domain"
|
||||
)
|
||||
|
||||
func TestLinkedDiscussionGuestAuthorizationFailsClosedMemory(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
channels := NewChannelStore()
|
||||
const (
|
||||
ownerID int64 = 9101
|
||||
outsiderID int64 = 9102
|
||||
)
|
||||
privateGroup, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: ownerID,
|
||||
Title: "unlinked private group",
|
||||
Megagroup: true,
|
||||
Date: 1700009100,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create private group: %v", err)
|
||||
}
|
||||
if _, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
||||
UserID: outsiderID,
|
||||
ChannelID: privateGroup.Channel.ID,
|
||||
RandomID: 91001,
|
||||
Message: "must be rejected",
|
||||
Date: 1700009101,
|
||||
}); !errors.Is(err, domain.ErrChannelPrivate) {
|
||||
t.Fatalf("unlinked outsider send err = %v, want ErrChannelPrivate", err)
|
||||
}
|
||||
if _, err := channels.ResolveDiscussionReadTarget(ctx, outsiderID, privateGroup.Channel.ID, privateGroup.Message.ID, privateGroup.Message.ID); !errors.Is(err, domain.ErrChannelPrivate) {
|
||||
t.Fatalf("unlinked outsider discussion read err = %v, want ErrChannelPrivate", err)
|
||||
}
|
||||
if got := len(channels.messages[privateGroup.Channel.ID]); got != 1 {
|
||||
t.Fatalf("unlinked outsider changed history length = %d, want create message only", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLinkedDiscussionGuestRepliesKeepViewerProjectionMemory(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
channels := NewChannelStore()
|
||||
const (
|
||||
ownerID int64 = 9201
|
||||
subscriberID int64 = 9202
|
||||
)
|
||||
broadcast, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: ownerID,
|
||||
Title: "linked source",
|
||||
Broadcast: true,
|
||||
Date: 1700009200,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create broadcast: %v", err)
|
||||
}
|
||||
group, err := channels.CreateChannel(ctx, domain.CreateChannelRequest{
|
||||
CreatorUserID: ownerID,
|
||||
Title: "linked discussion",
|
||||
Megagroup: true,
|
||||
Date: 1700009201,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("create group: %v", err)
|
||||
}
|
||||
if _, err := channels.SetDiscussionGroup(ctx, ownerID, broadcast.Channel.ID, group.Channel.ID); err != nil {
|
||||
t.Fatalf("set discussion group: %v", err)
|
||||
}
|
||||
if _, err := channels.InviteToChannel(ctx, broadcast.Channel.ID, ownerID, []int64{subscriberID}, 1700009202); err != nil {
|
||||
t.Fatalf("invite broadcast subscriber: %v", err)
|
||||
}
|
||||
post, err := channels.SendChannelMessage(ctx, domain.SendChannelMessageRequest{
|
||||
UserID: ownerID, ChannelID: broadcast.Channel.ID, RandomID: 92001,
|
||||
Message: "post", Date: 1700009203,
|
||||
})
|
||||
if err != nil || post.Discussion == nil {
|
||||
t.Fatalf("send linked post = %+v err %v", post, err)
|
||||
}
|
||||
|
||||
assertGuest := func(name string, history domain.ChannelHistory, err error) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
if history.Channel.ID != group.Channel.ID || history.Self.ChannelID != group.Channel.ID || history.Self.UserID != subscriberID || !history.Self.Guest || history.Self.Status != domain.ChannelMemberLeft {
|
||||
t.Fatalf("%s = channel %+v self %+v, want linked left guest", name, history.Channel, history.Self)
|
||||
}
|
||||
}
|
||||
direct, err := channels.ListChannelReplies(ctx, subscriberID, domain.ChannelRepliesFilter{
|
||||
ChannelID: group.Channel.ID, RootMessageID: post.Discussion.Message.ID, Limit: 20,
|
||||
})
|
||||
assertGuest("direct replies", direct, err)
|
||||
viaBroadcast, err := channels.ListChannelReplies(ctx, subscriberID, domain.ChannelRepliesFilter{
|
||||
ChannelID: broadcast.Channel.ID, RootMessageID: post.Message.ID, Limit: 20,
|
||||
})
|
||||
assertGuest("broadcast replies", viaBroadcast, err)
|
||||
|
||||
if _, err := channels.EditChannelBanned(ctx, domain.EditChannelBannedRequest{
|
||||
UserID: ownerID, ChannelID: group.Channel.ID,
|
||||
Participant: domain.Peer{Type: domain.PeerTypeUser, ID: subscriberID},
|
||||
BannedRights: domain.ChannelBannedRights{ViewMessages: true, UntilDate: 2147483647},
|
||||
Date: 1700009204,
|
||||
}); err != nil {
|
||||
t.Fatalf("ban linked subscriber from target: %v", err)
|
||||
}
|
||||
if _, err := channels.ListChannelReplies(ctx, subscriberID, domain.ChannelRepliesFilter{
|
||||
ChannelID: group.Channel.ID, RootMessageID: post.Discussion.Message.ID, Limit: 20,
|
||||
}); !errors.Is(err, domain.ErrChannelUserBanned) {
|
||||
t.Fatalf("target-banned direct replies err = %v, want ErrChannelUserBanned", err)
|
||||
}
|
||||
if _, err := channels.ListChannelReplies(ctx, subscriberID, domain.ChannelRepliesFilter{
|
||||
ChannelID: broadcast.Channel.ID, RootMessageID: post.Message.ID, Limit: 20,
|
||||
}); !errors.Is(err, domain.ErrChannelUserBanned) {
|
||||
t.Fatalf("target-banned broadcast replies err = %v, want ErrChannelUserBanned", err)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@ import (
|
|||
func (s *ChannelStore) GetParticipants(_ context.Context, viewerUserID, channelID int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.ChannelParticipantList, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
channel, viewer, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, channelID)
|
||||
channel, viewer, _, err := s.channelForViewerLocked(viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.ChannelParticipantList{}, err
|
||||
}
|
||||
|
|
@ -75,17 +75,16 @@ func (s *ChannelStore) GetParticipants(_ context.Context, viewerUserID, channelI
|
|||
func (s *ChannelStore) GetParticipant(_ context.Context, viewerUserID, channelID, participantUserID int64) (domain.ChannelMember, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
channel, viewer, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, channelID)
|
||||
_, viewer, _, err := s.channelForViewerLocked(viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.ChannelMember{}, err
|
||||
}
|
||||
if viewerUserID == participantUserID && viewer.Guest {
|
||||
return viewer, nil
|
||||
return domain.ChannelMember{}, domain.ErrUserNotParticipant
|
||||
}
|
||||
_ = channel
|
||||
member, ok := s.members[channelID][participantUserID]
|
||||
if !ok {
|
||||
return domain.ChannelMember{}, domain.ErrChannelPrivate
|
||||
if !ok || (participantUserID == viewerUserID && member.Status == domain.ChannelMemberLeft) {
|
||||
return domain.ChannelMember{}, domain.ErrUserNotParticipant
|
||||
}
|
||||
return member, nil
|
||||
}
|
||||
|
|
@ -868,7 +867,7 @@ func (s *ChannelStore) FilterActiveChannelMemberIDs(_ context.Context, channelID
|
|||
func (s *ChannelStore) ListActiveChannelMembers(_ context.Context, viewerUserID, channelID int64, limit int) (domain.Channel, domain.ChannelMember, []domain.ChannelMember, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
channel, viewer, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, channelID)
|
||||
channel, viewer, _, err := s.channelForViewerLocked(viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, nil, err
|
||||
}
|
||||
|
|
|
|||
|
|
@ -443,7 +443,16 @@ func (s *ChannelStore) ResolveDiscussionReadTarget(_ context.Context, viewerUser
|
|||
if targetChannelID != sourceChannelID {
|
||||
_, targetMember, err = s.channelAndMemberLocked(viewerUserID, targetChannelID)
|
||||
if errors.Is(err, domain.ErrChannelPrivate) {
|
||||
targetMember, _, err = s.linkedDiscussionGuestLocked(viewerUserID, s.channels[targetChannelID])
|
||||
guestMember, allowed, guestErr := s.linkedDiscussionGuestLocked(viewerUserID, s.channels[targetChannelID])
|
||||
switch {
|
||||
case guestErr != nil:
|
||||
err = guestErr
|
||||
case allowed:
|
||||
targetMember = guestMember
|
||||
err = nil
|
||||
default:
|
||||
err = domain.ErrChannelPrivate
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -41,10 +41,18 @@ func (s *ChannelStore) SendChannelMessage(_ context.Context, req domain.SendChan
|
|||
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
|
||||
if errors.Is(err, domain.ErrChannelPrivate) {
|
||||
if candidate, ok := s.channels[req.ChannelID]; ok && !candidate.Deleted {
|
||||
var guest bool
|
||||
member, guest, err = s.linkedDiscussionGuestLocked(req.UserID, candidate)
|
||||
if guest {
|
||||
guestMember, guest, guestErr := s.linkedDiscussionGuestLocked(req.UserID, candidate)
|
||||
switch {
|
||||
case guestErr != nil:
|
||||
err = guestErr
|
||||
case guest:
|
||||
channel = candidate
|
||||
member = guestMember
|
||||
err = nil
|
||||
default:
|
||||
// A clean "not a linked guest" result is not authorization.
|
||||
// Preserve the original private-member error and fail closed.
|
||||
err = domain.ErrChannelPrivate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -513,23 +513,19 @@ func (s *ChannelStore) ListChannelReplies(_ context.Context, viewerUserID int64,
|
|||
extraChannels := []domain.Channel(nil)
|
||||
if source.Broadcast {
|
||||
if root.Discussion == nil || root.Discussion.ChannelID == 0 || root.Discussion.MessageID == 0 {
|
||||
return domain.ChannelHistory{Channel: source, Count: 0}, nil
|
||||
return domain.ChannelHistory{Channel: source, Self: member, Count: 0}, nil
|
||||
}
|
||||
linked, ok := s.channels[root.Discussion.ChannelID]
|
||||
if !ok || linked.Deleted {
|
||||
return domain.ChannelHistory{Channel: source, Count: 0}, nil
|
||||
linked, linkedMember, linkedErr := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, root.Discussion.ChannelID)
|
||||
if linkedErr != nil {
|
||||
return domain.ChannelHistory{}, linkedErr
|
||||
}
|
||||
targetChannel = linked
|
||||
targetMember = linkedMember
|
||||
rootID = root.Discussion.MessageID
|
||||
if linkedMember, ok := s.members[linked.ID][viewerUserID]; ok {
|
||||
targetMember = linkedMember
|
||||
} else {
|
||||
targetMember = domain.ChannelMember{}
|
||||
}
|
||||
extraChannels = append(extraChannels, source)
|
||||
}
|
||||
if targetRoot, ok := s.findMessageLocked(targetChannel.ID, rootID); !ok || targetRoot.Deleted {
|
||||
return domain.ChannelHistory{Channel: targetChannel, Channels: extraChannels, Count: 0}, nil
|
||||
return domain.ChannelHistory{Channel: targetChannel, Self: targetMember, Channels: extraChannels, Count: 0}, nil
|
||||
}
|
||||
limit := filter.Limit
|
||||
if limit <= 0 || limit > domain.MaxChannelRepliesLimit {
|
||||
|
|
@ -570,7 +566,7 @@ func (s *ChannelStore) ListChannelReplies(_ context.Context, viewerUserID int64,
|
|||
topics = append(topics, cloneChannelForumTopic(topic))
|
||||
}
|
||||
}
|
||||
return domain.ChannelHistory{Channel: targetChannel, Channels: extraChannels, Topics: topics, Messages: out, Count: len(base)}, nil
|
||||
return domain.ChannelHistory{Channel: targetChannel, Self: targetMember, Channels: extraChannels, Topics: topics, Messages: out, Count: len(base)}, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) populateChannelMessageRepliesLocked(viewerUserID, channelID int64, messages []domain.ChannelMessage) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue