forum: let non-members browse a public forum's topic list
ListForumTopics / GetForumTopicsByID / GeneralForumTopic gated on membership while channel history uses the public-preview path, so a public forum's topics (General included) were invisible until you joined. Switch them to getChannelForViewer / channelForViewerLocked; private forums and write paths keep the membership gate.
This commit is contained in:
parent
f26468ef6d
commit
6d34843fd0
5 changed files with 108 additions and 6 deletions
98
internal/rpc/forum_topics_preview_rpc_test.go
Normal file
98
internal/rpc/forum_topics_preview_rpc_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
package rpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/iamxvbaba/td/clock"
|
||||||
|
"github.com/iamxvbaba/td/tg"
|
||||||
|
"go.uber.org/zap/zaptest"
|
||||||
|
|
||||||
|
appchannels "telesrv/internal/app/channels"
|
||||||
|
appusers "telesrv/internal/app/users"
|
||||||
|
"telesrv/internal/domain"
|
||||||
|
"telesrv/internal/store/memory"
|
||||||
|
)
|
||||||
|
|
||||||
|
// A public forum's topic list is browsable before joining, like its history.
|
||||||
|
// Regression: getForumTopics used the member-only access path and returned
|
||||||
|
// CHANNEL_PRIVATE / an empty list to non-members, so the topic list (and even
|
||||||
|
// General) was invisible until they joined.
|
||||||
|
func TestGetForumTopicsVisibleToPublicNonMember(t *testing.T) {
|
||||||
|
ctx := context.Background()
|
||||||
|
userStore := memory.NewUserStore()
|
||||||
|
owner, _ := userStore.Create(ctx, domain.User{AccessHash: 81, Phone: "15550008101", FirstName: "Owner"})
|
||||||
|
outsider, _ := userStore.Create(ctx, domain.User{AccessHash: 82, Phone: "15550008102", FirstName: "Outsider"})
|
||||||
|
channelStore := memory.NewChannelStore()
|
||||||
|
channelSvc := appchannels.NewService(channelStore)
|
||||||
|
r := New(Config{}, Deps{
|
||||||
|
Users: appusers.NewService(userStore),
|
||||||
|
Channels: channelSvc,
|
||||||
|
}, zaptest.NewLogger(t), clock.System)
|
||||||
|
|
||||||
|
ownerCtx := WithUserID(ctx, owner.ID)
|
||||||
|
outsiderCtx := WithUserID(ctx, outsider.ID)
|
||||||
|
|
||||||
|
created, err := r.onChannelsCreateChannel(ownerCtx, &tg.ChannelsCreateChannelRequest{Title: "Public Forum", Megagroup: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create channel: %v", err)
|
||||||
|
}
|
||||||
|
channel := created.(*tg.Updates).Chats[0].(*tg.Channel)
|
||||||
|
input := &tg.InputChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||||
|
forumPeer := &tg.InputPeerChannel{ChannelID: channel.ID, AccessHash: channel.AccessHash}
|
||||||
|
|
||||||
|
if _, err := r.onChannelsToggleForum(ownerCtx, &tg.ChannelsToggleForumRequest{Channel: input, Enabled: true, Tabs: true}); err != nil {
|
||||||
|
t.Fatalf("toggle forum: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := channelSvc.UpdateUsername(ctx, owner.ID, domain.UpdateChannelUsernameRequest{
|
||||||
|
ChannelID: channel.ID,
|
||||||
|
Username: "publicforum",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("set channel username: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := r.onMessagesCreateForumTopic(ownerCtx, &tg.MessagesCreateForumTopicRequest{
|
||||||
|
Peer: forumPeer,
|
||||||
|
Title: "Test",
|
||||||
|
IconColor: domain.DefaultForumTopicIconColor,
|
||||||
|
RandomID: 8101001,
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatalf("create forum topic: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
res, err := r.onMessagesGetForumTopics(outsiderCtx, &tg.MessagesGetForumTopicsRequest{
|
||||||
|
Peer: forumPeer,
|
||||||
|
Limit: 100,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("getForumTopics as non-member: %v", err)
|
||||||
|
}
|
||||||
|
titles := map[string]bool{}
|
||||||
|
for _, tc := range res.Topics {
|
||||||
|
switch topic := tc.(type) {
|
||||||
|
case *tg.ForumTopic:
|
||||||
|
titles[topic.Title] = true
|
||||||
|
case *tg.ForumTopicDeleted:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !titles["General"] {
|
||||||
|
t.Fatalf("non-member did not see the General topic: %+v", res.Topics)
|
||||||
|
}
|
||||||
|
if !titles["Test"] {
|
||||||
|
t.Fatalf("non-member did not see the Test topic: %+v", res.Topics)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A private forum still refuses a non-member.
|
||||||
|
priv, err := r.onChannelsCreateChannel(ownerCtx, &tg.ChannelsCreateChannelRequest{Title: "Private Forum", Megagroup: true})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("create private channel: %v", err)
|
||||||
|
}
|
||||||
|
privCh := priv.(*tg.Updates).Chats[0].(*tg.Channel)
|
||||||
|
privInput := &tg.InputChannel{ChannelID: privCh.ID, AccessHash: privCh.AccessHash}
|
||||||
|
privPeer := &tg.InputPeerChannel{ChannelID: privCh.ID, AccessHash: privCh.AccessHash}
|
||||||
|
if _, err := r.onChannelsToggleForum(ownerCtx, &tg.ChannelsToggleForumRequest{Channel: privInput, Enabled: true, Tabs: true}); err != nil {
|
||||||
|
t.Fatalf("toggle private forum: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := r.onMessagesGetForumTopics(outsiderCtx, &tg.MessagesGetForumTopicsRequest{Peer: privPeer, Limit: 100}); err == nil {
|
||||||
|
t.Fatal("non-member read a private forum's topic list")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -178,7 +178,7 @@ func (s *ChannelStore) GeneralForumTopic(_ context.Context, viewerUserID, channe
|
||||||
}
|
}
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
channel, member, err := s.channelAndMemberLocked(viewerUserID, channelID)
|
channel, member, _, err := s.channelForViewerLocked(viewerUserID, channelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ChannelForumTopic{}, err
|
return domain.ChannelForumTopic{}, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -420,7 +420,9 @@ func (s *ChannelStore) DeleteForumTopicHistory(_ context.Context, req domain.Del
|
||||||
func (s *ChannelStore) ListForumTopics(_ context.Context, viewerUserID int64, filter domain.ChannelForumTopicFilter) (domain.ChannelForumTopicList, error) {
|
func (s *ChannelStore) ListForumTopics(_ context.Context, viewerUserID int64, filter domain.ChannelForumTopicFilter) (domain.ChannelForumTopicList, error) {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
channel, member, err := s.channelAndMemberLocked(viewerUserID, filter.ChannelID)
|
// channelForViewerLocked, not channelAndMemberLocked: a public forum's topic
|
||||||
|
// list is browsable before joining, exactly like its message history.
|
||||||
|
channel, member, _, err := s.channelForViewerLocked(viewerUserID, filter.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ChannelForumTopicList{}, err
|
return domain.ChannelForumTopicList{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -463,7 +465,7 @@ func (s *ChannelStore) ListForumTopics(_ context.Context, viewerUserID int64, fi
|
||||||
func (s *ChannelStore) GetForumTopicsByID(_ context.Context, viewerUserID, channelID int64, ids []int) (domain.ChannelForumTopicList, error) {
|
func (s *ChannelStore) GetForumTopicsByID(_ context.Context, viewerUserID, channelID int64, ids []int) (domain.ChannelForumTopicList, error) {
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
defer s.mu.RUnlock()
|
defer s.mu.RUnlock()
|
||||||
channel, member, err := s.channelAndMemberLocked(viewerUserID, channelID)
|
channel, member, _, err := s.channelForViewerLocked(viewerUserID, channelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ChannelForumTopicList{}, err
|
return domain.ChannelForumTopicList{}, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -198,7 +198,7 @@ func (s *ChannelStore) GeneralForumTopic(ctx context.Context, viewerUserID, chan
|
||||||
if viewerUserID == 0 || channelID == 0 {
|
if viewerUserID == 0 || channelID == 0 {
|
||||||
return domain.ChannelForumTopic{}, domain.ErrChannelInvalid
|
return domain.ChannelForumTopic{}, domain.ErrChannelInvalid
|
||||||
}
|
}
|
||||||
channel, member, err := s.getChannelForMember(ctx, s.db, viewerUserID, channelID)
|
channel, member, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ChannelForumTopic{}, err
|
return domain.ChannelForumTopic{}, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -470,7 +470,9 @@ WHERE channel_id = $1 AND topic_id = $2`, req.ChannelID, req.TopicID); err != ni
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ChannelStore) ListForumTopics(ctx context.Context, viewerUserID int64, filter domain.ChannelForumTopicFilter) (domain.ChannelForumTopicList, error) {
|
func (s *ChannelStore) ListForumTopics(ctx context.Context, viewerUserID int64, filter domain.ChannelForumTopicFilter) (domain.ChannelForumTopicList, error) {
|
||||||
channel, member, err := s.getChannelForMember(ctx, s.db, viewerUserID, filter.ChannelID)
|
// getChannelForViewer, not getChannelForMember: a public forum's topic list is
|
||||||
|
// browsable before joining, exactly like its message history.
|
||||||
|
channel, member, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, filter.ChannelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ChannelForumTopicList{}, err
|
return domain.ChannelForumTopicList{}, err
|
||||||
}
|
}
|
||||||
|
|
@ -539,7 +541,7 @@ LIMIT $`+fmt.Sprint(len(args)), args...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ChannelStore) GetForumTopicsByID(ctx context.Context, viewerUserID, channelID int64, ids []int) (domain.ChannelForumTopicList, error) {
|
func (s *ChannelStore) GetForumTopicsByID(ctx context.Context, viewerUserID, channelID int64, ids []int) (domain.ChannelForumTopicList, error) {
|
||||||
channel, member, err := s.getChannelForMember(ctx, s.db, viewerUserID, channelID)
|
channel, member, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return domain.ChannelForumTopicList{}, err
|
return domain.ChannelForumTopicList{}, err
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue