fix: sync linked discussion guest support
This commit is contained in:
parent
5f7c0b9804
commit
9f73dc20da
27 changed files with 1004 additions and 30 deletions
|
|
@ -124,6 +124,38 @@ func (s *ChannelStore) GetChannel(_ context.Context, viewerUserID, channelID int
|
|||
}, nil
|
||||
}
|
||||
|
||||
// GetLinkedDiscussionChannel returns the discussion peer through an active
|
||||
// membership in its source broadcast channel. It deliberately does not turn
|
||||
// the viewer into a discussion-group member: callers need the Left projection
|
||||
// so clients can show the comment entry and, when needed, the Join Group gate.
|
||||
func (s *ChannelStore) GetLinkedDiscussionChannel(_ context.Context, viewerUserID, sourceChannelID int64) (domain.ChannelView, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
source, _, err := s.channelAndMemberLocked(viewerUserID, sourceChannelID)
|
||||
if err != nil {
|
||||
return domain.ChannelView{}, err
|
||||
}
|
||||
if !source.Broadcast || source.LinkedChatID == 0 {
|
||||
return domain.ChannelView{}, domain.ErrChannelInvalid
|
||||
}
|
||||
linked, ok := s.channels[source.LinkedChatID]
|
||||
if !ok || linked.Deleted || !linked.Megagroup || linked.Broadcast {
|
||||
return domain.ChannelView{}, domain.ErrChannelInvalid
|
||||
}
|
||||
self, guest, guestErr := s.linkedDiscussionGuestLocked(viewerUserID, linked)
|
||||
if guestErr != nil {
|
||||
return domain.ChannelView{}, guestErr
|
||||
}
|
||||
if !guest {
|
||||
if member, ok := s.members[linked.ID][viewerUserID]; ok {
|
||||
self = member
|
||||
} else {
|
||||
return domain.ChannelView{}, domain.ErrChannelPrivate
|
||||
}
|
||||
}
|
||||
return domain.ChannelView{Channel: cloneChannel(linked), Self: self}, nil
|
||||
}
|
||||
|
||||
// ResolveChannel 是 GetChannel 的轻量版:只做访问校验并返回 Channel+Self,跳过 dialog/boost。
|
||||
// 与 postgres 实现语义一致(内存侧 dialog/boost 本就便宜,但保持接口行为对齐)。
|
||||
func (s *ChannelStore) ResolveChannel(_ context.Context, viewerUserID, channelID int64) (domain.ChannelView, error) {
|
||||
|
|
|
|||
|
|
@ -359,6 +359,11 @@ func (s *ChannelStore) channelForViewerLocked(userID, channelID int64) (domain.C
|
|||
if found && (existing.Status == domain.ChannelMemberBanned || existing.Status == domain.ChannelMemberKicked || existing.BannedRights.ViewMessages) {
|
||||
return domain.Channel{}, domain.ChannelMember{}, false, domain.ErrChannelUserBanned
|
||||
}
|
||||
if guest, ok, guestErr := s.linkedDiscussionGuestLocked(userID, channel); guestErr != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, false, guestErr
|
||||
} else if ok {
|
||||
return channel, guest, true, nil
|
||||
}
|
||||
if channel.Monoforum && channel.LinkedMonoforumID != 0 {
|
||||
parentMember, ok := s.members[channel.LinkedMonoforumID][userID]
|
||||
if ok && parentMember.Status == domain.ChannelMemberActive && isChannelAdmin(parentMember) {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package memory
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -11,14 +12,13 @@ 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, err := s.channelForMemberLocked(viewerUserID, channelID)
|
||||
channel, viewer, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.ChannelParticipantList{}, err
|
||||
}
|
||||
if limit <= 0 || limit > domain.MaxChannelParticipantsLimit {
|
||||
limit = domain.MaxChannelParticipantsLimit
|
||||
}
|
||||
viewer := s.members[channelID][viewerUserID]
|
||||
// 广播频道订阅者列表仅管理员可枚举(与隐藏成员同一门控):admins filter 仍放行(徽章数据源)。
|
||||
if channel.MembersListAdminOnly() && !isChannelAdmin(viewer) {
|
||||
switch filter.Kind {
|
||||
|
|
@ -75,9 +75,14 @@ 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()
|
||||
if _, err := s.channelForMemberLocked(viewerUserID, channelID); err != nil {
|
||||
channel, viewer, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.ChannelMember{}, err
|
||||
}
|
||||
if viewerUserID == participantUserID && viewer.Guest {
|
||||
return viewer, nil
|
||||
}
|
||||
_ = channel
|
||||
member, ok := s.members[channelID][participantUserID]
|
||||
if !ok {
|
||||
return domain.ChannelMember{}, domain.ErrChannelPrivate
|
||||
|
|
@ -824,7 +829,7 @@ func (s *ChannelStore) recordPublicJoinRequestLocked(channel domain.Channel, use
|
|||
func (s *ChannelStore) ListActiveChannelMemberIDs(_ context.Context, viewerUserID, channelID int64, limit int) ([]int64, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
if _, err := s.channelForMemberLocked(viewerUserID, channelID); err != nil {
|
||||
if _, _, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, channelID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.activeMemberIDsLocked(channelID, 0, limit), nil
|
||||
|
|
@ -863,7 +868,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.channelAndMemberLocked(viewerUserID, channelID)
|
||||
channel, viewer, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, channelID)
|
||||
if err != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, nil, err
|
||||
}
|
||||
|
|
@ -905,6 +910,57 @@ func (s *ChannelStore) channelAndMemberLocked(userID, channelID int64) (domain.C
|
|||
return channel, member, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) channelAndMemberOrLinkedGuestLocked(userID, channelID int64) (domain.Channel, domain.ChannelMember, error) {
|
||||
channel, member, err := s.channelAndMemberLocked(userID, channelID)
|
||||
if !errors.Is(err, domain.ErrChannelPrivate) {
|
||||
return channel, member, err
|
||||
}
|
||||
target, ok := s.channels[channelID]
|
||||
if !ok || target.Deleted {
|
||||
return domain.Channel{}, domain.ChannelMember{}, domain.ErrChannelInvalid
|
||||
}
|
||||
guest, allowed, guestErr := s.linkedDiscussionGuestLocked(userID, target)
|
||||
if guestErr != nil {
|
||||
return domain.Channel{}, domain.ChannelMember{}, guestErr
|
||||
}
|
||||
if !allowed {
|
||||
return domain.Channel{}, domain.ChannelMember{}, err
|
||||
}
|
||||
return target, guest, nil
|
||||
}
|
||||
|
||||
// linkedDiscussionGuestLocked authorizes a discussion-group view through the
|
||||
// viewer's active membership in its bidirectionally linked broadcast. It never
|
||||
// materializes a channel_members row. Explicit target bans always win.
|
||||
func (s *ChannelStore) linkedDiscussionGuestLocked(userID int64, target domain.Channel) (domain.ChannelMember, bool, error) {
|
||||
if target.Broadcast || !target.Megagroup || target.LinkedChatID == 0 {
|
||||
return domain.ChannelMember{}, false, nil
|
||||
}
|
||||
if existing, ok := s.members[target.ID][userID]; ok {
|
||||
if existing.Status == domain.ChannelMemberBanned || existing.Status == domain.ChannelMemberKicked || existing.BannedRights.ViewMessages {
|
||||
return domain.ChannelMember{}, false, domain.ErrChannelUserBanned
|
||||
}
|
||||
if existing.Status == domain.ChannelMemberActive {
|
||||
return existing, false, nil
|
||||
}
|
||||
}
|
||||
source, ok := s.channels[target.LinkedChatID]
|
||||
if !ok || source.Deleted || !source.Broadcast || source.LinkedChatID != target.ID {
|
||||
return domain.ChannelMember{}, false, nil
|
||||
}
|
||||
sourceMember, ok := s.members[source.ID][userID]
|
||||
if !ok || sourceMember.Status != domain.ChannelMemberActive || sourceMember.BannedRights.ViewMessages {
|
||||
return domain.ChannelMember{}, false, nil
|
||||
}
|
||||
return domain.ChannelMember{
|
||||
ChannelID: target.ID,
|
||||
UserID: userID,
|
||||
Status: domain.ChannelMemberLeft,
|
||||
Role: domain.ChannelRoleMember,
|
||||
Guest: true,
|
||||
}, true, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) activeMemberIDsLocked(channelID, excludeUserID int64, limit int) []int64 {
|
||||
members := s.members[channelID]
|
||||
if limit <= 0 || limit > domain.MaxChannelRealtimeFanout {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package memory
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"sort"
|
||||
"strings"
|
||||
"telesrv/internal/domain"
|
||||
|
|
@ -413,6 +414,53 @@ func (s *ChannelStore) GetDiscussionMessage(_ context.Context, viewerUserID, cha
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// ResolveDiscussionReadTarget returns the minimal linked-thread mapping and
|
||||
// current read boundary without building reply/reaction aggregates.
|
||||
func (s *ChannelStore) ResolveDiscussionReadTarget(_ context.Context, viewerUserID, sourceChannelID int64, sourceMessageID, readMaxID int) (domain.ChannelDiscussionReadTarget, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
source, sourceMember, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, sourceChannelID)
|
||||
if err != nil {
|
||||
return domain.ChannelDiscussionReadTarget{}, err
|
||||
}
|
||||
msg, ok := s.findMessageLocked(sourceChannelID, sourceMessageID)
|
||||
if !ok || msg.Deleted || msg.ID <= sourceMember.AvailableMinID {
|
||||
return domain.ChannelDiscussionReadTarget{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
targetChannelID, rootID := sourceChannelID, sourceMessageID
|
||||
if source.Broadcast && msg.Discussion != nil {
|
||||
if msg.Discussion.ChannelID == 0 || msg.Discussion.MessageID == 0 {
|
||||
return domain.ChannelDiscussionReadTarget{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
linked, exists := s.channels[msg.Discussion.ChannelID]
|
||||
root, rootExists := s.findMessageLocked(msg.Discussion.ChannelID, msg.Discussion.MessageID)
|
||||
if !exists || linked.Deleted || !linked.Megagroup || linked.Broadcast || !rootExists || root.Deleted {
|
||||
return domain.ChannelDiscussionReadTarget{}, domain.ErrMessageIDInvalid
|
||||
}
|
||||
targetChannelID, rootID = linked.ID, root.ID
|
||||
}
|
||||
targetMember := sourceMember
|
||||
if targetChannelID != sourceChannelID {
|
||||
_, targetMember, err = s.channelAndMemberLocked(viewerUserID, targetChannelID)
|
||||
if errors.Is(err, domain.ErrChannelPrivate) {
|
||||
targetMember, _, err = s.linkedDiscussionGuestLocked(viewerUserID, s.channels[targetChannelID])
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return domain.ChannelDiscussionReadTarget{}, err
|
||||
}
|
||||
effectiveMaxID := readMaxID
|
||||
if target := s.channels[targetChannelID]; effectiveMaxID <= 0 || effectiveMaxID > target.TopMessageID {
|
||||
effectiveMaxID = target.TopMessageID
|
||||
}
|
||||
return domain.ChannelDiscussionReadTarget{
|
||||
ChannelID: targetChannelID,
|
||||
RootID: rootID,
|
||||
AlreadyRead: effectiveMaxID <= targetMember.ReadInboxMaxID && !targetMember.UnreadMark,
|
||||
Guest: targetMember.Guest,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ChannelStore) findMessageLocked(channelID int64, id int) (domain.ChannelMessage, bool) {
|
||||
for _, msg := range s.messages[channelID] {
|
||||
if msg.ID == id {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ package memory
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"telesrv/internal/domain"
|
||||
|
|
@ -37,11 +38,22 @@ func (s *ChannelStore) SendChannelMessage(_ context.Context, req domain.SendChan
|
|||
return replay, replayErr
|
||||
}
|
||||
}
|
||||
channel, err := s.channelForMemberLocked(req.UserID, req.ChannelID)
|
||||
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 {
|
||||
channel = candidate
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return domain.SendChannelMessageResult{}, err
|
||||
}
|
||||
member := s.members[req.ChannelID][req.UserID]
|
||||
if member.Guest && channel.JoinToSend {
|
||||
return domain.SendChannelMessageResult{}, domain.ErrChannelWriteForbidden
|
||||
}
|
||||
if req.Date == 0 {
|
||||
req.Date = int(time.Now().Unix())
|
||||
}
|
||||
|
|
@ -198,8 +210,10 @@ func (s *ChannelStore) SendChannelMessage(_ context.Context, req domain.SendChan
|
|||
channel.TopMessageID = msg.ID
|
||||
channel.Pts = pts
|
||||
s.channels[req.ChannelID] = channel
|
||||
member.SlowmodeLastSendDate = req.Date
|
||||
s.members[req.ChannelID][req.UserID] = member
|
||||
if !member.Guest {
|
||||
member.SlowmodeLastSendDate = req.Date
|
||||
s.members[req.ChannelID][req.UserID] = member
|
||||
}
|
||||
for userID, member := range s.members[req.ChannelID] {
|
||||
if member.Status == domain.ChannelMemberActive {
|
||||
if _, skip := skipDelivery[userID]; skip && userID != req.UserID {
|
||||
|
|
|
|||
|
|
@ -109,13 +109,16 @@ func (s *ChannelStore) ReadChannelTopicHistory(_ context.Context, req domain.Rea
|
|||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
channel, member, err := s.channelAndMemberLocked(req.UserID, req.ChannelID)
|
||||
channel, member, err := s.channelAndMemberOrLinkedGuestLocked(req.UserID, req.ChannelID)
|
||||
if err != nil {
|
||||
return domain.ReadChannelTopicHistoryResult{}, err
|
||||
}
|
||||
if !channel.Forum {
|
||||
return domain.ReadChannelTopicHistoryResult{}, domain.ErrChannelForumMissing
|
||||
}
|
||||
if member.Guest {
|
||||
return domain.ReadChannelTopicHistoryResult{}, domain.ErrChannelPrivate
|
||||
}
|
||||
topMax := s.channelTopicTopMessageIDLocked(req.ChannelID, req.TopicID, member.AvailableMinID)
|
||||
maxID := req.MaxID
|
||||
if maxID <= 0 || maxID > topMax {
|
||||
|
|
|
|||
|
|
@ -499,7 +499,7 @@ func (s *ChannelStore) GetForumTopicsByID(_ context.Context, viewerUserID, chann
|
|||
func (s *ChannelStore) ListChannelReplies(_ context.Context, viewerUserID int64, filter domain.ChannelRepliesFilter) (domain.ChannelHistory, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
source, member, err := s.channelAndMemberLocked(viewerUserID, filter.ChannelID)
|
||||
source, member, err := s.channelAndMemberOrLinkedGuestLocked(viewerUserID, filter.ChannelID)
|
||||
if err != nil {
|
||||
return domain.ChannelHistory{}, err
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue