owpengram-server/internal/app/channels/bot_policy.go

363 lines
11 KiB
Go

package channels
import (
"context"
"sort"
"strings"
"telesrv/internal/domain"
)
// BotProfileResolver is the domain-only view of bot metadata used by channel policy.
type BotProfileResolver interface {
BotInfo(ctx context.Context, botUserID int64) (domain.BotProfile, bool, error)
}
type botProfileBatchResolver interface {
BotInfos(ctx context.Context, botUserIDs []int64) (map[int64]domain.BotProfile, error)
}
type activeChannelBotMemberLister interface {
ListActiveChannelBotMembers(ctx context.Context, viewerUserID, channelID int64, offset, limit int) (domain.ChannelParticipantList, error)
}
type activeChannelBotMemberIDLister interface {
ListActiveChannelBotMemberIDs(ctx context.Context, viewerUserID, channelID int64, limit int) ([]int64, error)
}
func (s *Service) getBotParticipants(ctx context.Context, userID, channelID int64, offset, limit int) (domain.ChannelParticipantList, error) {
if lister, ok := s.channels.(activeChannelBotMemberLister); ok {
return lister.ListActiveChannelBotMembers(ctx, userID, channelID, offset, limit)
}
channel, viewer, active, err := s.channels.ListActiveChannelMembers(ctx, userID, channelID, domain.MaxSynchronousChannelDialogFanout)
if err != nil {
return domain.ChannelParticipantList{}, err
}
if channel.ParticipantsHidden && !channelServiceMemberIsAdmin(viewer) {
return domain.ChannelParticipantList{Channel: channel, Count: 0}, nil
}
if offset < 0 {
offset = 0
}
if offset > domain.MaxChannelParticipantsOffset {
offset = domain.MaxChannelParticipantsOffset
}
ids := make([]int64, 0, len(active))
for _, member := range active {
if member.UserID != 0 {
ids = append(ids, member.UserID)
}
}
profiles, err := s.botProfiles(ctx, ids)
if err != nil {
return domain.ChannelParticipantList{}, err
}
members := make([]domain.ChannelMember, 0, limit)
count := 0
for _, member := range active {
if _, found := profiles[member.UserID]; !found {
continue
}
if count >= offset && len(members) < limit {
members = append(members, member)
}
count++
}
return domain.ChannelParticipantList{Channel: channel, Participants: members, Count: count}, nil
}
func (s *Service) botProfiles(ctx context.Context, ids []int64) (map[int64]domain.BotProfile, error) {
if len(ids) == 0 || s.bots == nil {
return nil, nil
}
if batch, ok := s.bots.(botProfileBatchResolver); ok {
return batch.BotInfos(ctx, ids)
}
out := make(map[int64]domain.BotProfile)
for _, id := range uniqueNonZero(ids) {
profile, found, err := s.bots.BotInfo(ctx, id)
if err != nil {
return nil, err
}
if found {
out[id] = profile
}
}
return out, nil
}
func (s *Service) rejectBlockedBotInvites(ctx context.Context, userIDs []int64) error {
if s.bots == nil {
return nil
}
for _, id := range uniqueNonZero(userIDs) {
profile, found, err := s.bots.BotInfo(ctx, id)
if err != nil {
return err
}
if found && profile.Nochats {
return domain.ErrBotGroupsBlocked
}
}
return nil
}
func (s *Service) skippedBotDeliveryUserIDs(ctx context.Context, req domain.SendChannelMessageRequest) ([]int64, error) {
if s.bots == nil || req.ChannelID == 0 || req.UserID == 0 {
return nil, nil
}
memberIDs, err := s.loadActiveBotMemberIDs(ctx, req.UserID, req.ChannelID, domain.MaxSynchronousChannelDialogFanout)
if err != nil {
return nil, err
}
return s.skippedBotDeliveryUserIDsForIDs(ctx, req, memberIDs)
}
// ActiveBotMemberIDs returns active bot members for non-privacy-critical producers
// such as Bot API update queue fanout. Privacy delivery decisions use
// loadActiveBotMemberIDs directly to avoid stale-cache leaks.
func (s *Service) ActiveBotMemberIDs(ctx context.Context, viewerUserID, channelID int64, limit int) ([]int64, error) {
if s == nil || s.channels == nil || viewerUserID == 0 || channelID == 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxSynchronousChannelDialogFanout {
limit = domain.MaxSynchronousChannelDialogFanout
}
key := activeBotMemberIDsCacheKey{viewerUserID: viewerUserID, channelID: channelID, limit: limit}
if s.versions != nil {
hash, err := s.channelBotMemberIDsHash(ctx, viewerUserID, channelID, key)
if err != nil {
return nil, err
}
if hash != 0 {
return s.botMemberIDsCache.getOrLoadVersioned(ctx, key, hash, func() ([]int64, error) {
return s.loadActiveBotMemberIDs(ctx, viewerUserID, channelID, limit)
})
}
return s.loadActiveBotMemberIDs(ctx, viewerUserID, channelID, limit)
}
return s.botMemberIDsCache.getOrLoad(ctx, key, func() ([]int64, error) {
return s.loadActiveBotMemberIDs(ctx, viewerUserID, channelID, limit)
})
}
func (s *Service) loadActiveBotMemberIDs(ctx context.Context, viewerUserID, channelID int64, limit int) ([]int64, error) {
if s == nil || s.channels == nil || viewerUserID == 0 || channelID == 0 {
return nil, domain.ErrChannelInvalid
}
if limit <= 0 || limit > domain.MaxSynchronousChannelDialogFanout {
limit = domain.MaxSynchronousChannelDialogFanout
}
if lister, ok := s.channels.(activeChannelBotMemberIDLister); ok {
return lister.ListActiveChannelBotMemberIDs(ctx, viewerUserID, channelID, limit)
}
if s.bots == nil {
return nil, nil
}
memberIDs, err := s.channels.ListActiveChannelMemberIDs(ctx, viewerUserID, channelID, limit)
if err != nil {
return nil, err
}
profiles, err := s.botProfiles(ctx, memberIDs)
if err != nil {
return nil, err
}
out := make([]int64, 0, len(profiles))
for _, id := range uniqueNonZero(memberIDs) {
if _, found := profiles[id]; found {
out = append(out, id)
}
}
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out, nil
}
func (s *Service) skippedBotDeliveryUserIDsForIDs(ctx context.Context, req domain.SendChannelMessageRequest, memberIDs []int64) ([]int64, error) {
profiles, err := s.botProfiles(ctx, memberIDs)
if err != nil {
return nil, err
}
msg := domain.ChannelMessage{
ChannelID: req.ChannelID,
SenderUserID: req.UserID,
Body: req.Message,
ReplyTo: req.ReplyTo,
Action: req.Action,
}
skip := make([]int64, 0)
for _, id := range memberIDs {
if id == req.UserID {
continue
}
profile, found := profiles[id]
if !found || profile.ChatHistory {
continue
}
visible, err := s.botCanSeeChannelMessage(ctx, id, msg, req.MentionUserIDs)
if err != nil {
return nil, err
}
if !visible {
skip = append(skip, id)
}
}
return skip, nil
}
func (s *Service) filterBotChannelHistory(ctx context.Context, userID int64, history domain.ChannelHistory) domain.ChannelHistory {
if s.bots == nil || userID == 0 || history.Channel.ID == 0 {
return history
}
profile, found, err := s.bots.BotInfo(ctx, userID)
if err != nil || !found || profile.ChatHistory {
return history
}
filtered := history
filtered.Messages = make([]domain.ChannelMessage, 0, len(history.Messages))
for _, msg := range history.Messages {
if visible, err := s.botCanSeeChannelMessage(ctx, userID, msg, nil); err == nil && visible {
filtered.Messages = append(filtered.Messages, msg)
}
}
filtered.Count = len(filtered.Messages)
filtered.Users = nil
filtered.Channels = nil
return filtered
}
func (s *Service) filterBotChannelDifference(ctx context.Context, userID int64, diff domain.ChannelDifference) domain.ChannelDifference {
if s.bots == nil || userID == 0 || diff.Channel.ID == 0 {
return diff
}
profile, found, err := s.bots.BotInfo(ctx, userID)
if err != nil || !found || profile.ChatHistory {
return diff
}
filtered := diff
filtered.NewMessages = nil
filtered.OtherUpdates = nil
filtered.Events = nil
filtered.Users = nil
filtered.Channels = nil
if diff.TooLong {
for _, msg := range diff.NewMessages {
if visible, err := s.botCanSeeChannelMessage(ctx, userID, msg, nil); err == nil && visible {
filtered.NewMessages = append(filtered.NewMessages, msg)
}
}
return filtered
}
for _, event := range diff.Events {
visibleEvent, ok := s.filterBotChannelEvent(ctx, userID, event)
if !ok {
continue
}
filtered.Events = append(filtered.Events, visibleEvent)
switch visibleEvent.Type {
case domain.ChannelUpdateNewMessage:
filtered.NewMessages = append(filtered.NewMessages, visibleEvent.Message)
default:
filtered.OtherUpdates = append(filtered.OtherUpdates, visibleEvent)
}
}
return filtered
}
func (s *Service) filterBotChannelEvent(ctx context.Context, botUserID int64, event domain.ChannelUpdateEvent) (domain.ChannelUpdateEvent, bool) {
switch event.Type {
case domain.ChannelUpdateNewMessage, domain.ChannelUpdateEditMessage:
if event.Message.ID == 0 {
return event, true
}
visible, err := s.botCanSeeChannelMessage(ctx, botUserID, event.Message, nil)
if err != nil || !visible {
return domain.ChannelUpdateEvent{}, false
}
return event, true
case domain.ChannelUpdateDeleteMessages:
// 删除事件只携带消息 id、不含任何内容,且在线推送路径(channels_updates 的
// channelDeleteMessagesUpdates→enqueueChannelFanout)本就对全体成员无差别投递删除。
// 若在此按可见性过滤,会因被删消息无法重取(GetChannelMessages 带 AND NOT deleted
// 恒返空)而把整条 delete 事件丢弃——privacy bot 经 getChannelDifference 补差时将
// 对所有删除失明(连它本可见消息的删除也收不到),客户端缓存残留"未删"态。故直接放行,
// 与在线推送行为一致(删除 id 不泄漏内容)。
return event, true
case domain.ChannelUpdatePinnedMessages:
if len(event.MessageIDs) == 0 {
return event, true
}
ids := make([]int, 0, len(event.MessageIDs))
for _, id := range event.MessageIDs {
history, err := s.channels.GetChannelMessages(ctx, botUserID, event.ChannelID, []int{id})
if err != nil || len(history.Messages) == 0 {
continue
}
visible, err := s.botCanSeeChannelMessage(ctx, botUserID, history.Messages[0], nil)
if err == nil && visible {
ids = append(ids, id)
}
}
if len(ids) == 0 {
return domain.ChannelUpdateEvent{}, false
}
event.MessageIDs = ids
return event, true
default:
return event, true
}
}
func (s *Service) botCanSeeChannelMessage(ctx context.Context, botUserID int64, msg domain.ChannelMessage, mentionUserIDs []int64) (bool, error) {
if botUserID == 0 {
return true, nil
}
if msg.SenderUserID == botUserID {
return true, nil
}
if msg.Mentioned || containsInt64(mentionUserIDs, botUserID) {
return true, nil
}
if msg.Action != nil && containsInt64(msg.Action.UserIDs, botUserID) {
return true, nil
}
if messageIsCommand(msg.Body) {
return true, nil
}
if msg.ReplyTo != nil && msg.ReplyTo.MessageID > 0 {
history, err := s.channels.GetChannelMessages(ctx, botUserID, msg.ChannelID, []int{msg.ReplyTo.MessageID})
if err != nil {
return false, nil
}
for _, target := range history.Messages {
if target.SenderUserID == botUserID {
return true, nil
}
}
}
return false, nil
}
func messageIsCommand(message string) bool {
message = strings.TrimSpace(message)
return strings.HasPrefix(message, "/") && len(message) > 1
}
func containsInt64(ids []int64, target int64) bool {
for _, id := range ids {
if id == target {
return true
}
}
return false
}
func channelServiceMemberIsAdmin(member domain.ChannelMember) bool {
return member.Role == domain.ChannelRoleCreator || member.Role == domain.ChannelRoleAdmin
}
func mergeSkippedUserIDs(a, b []int64) []int64 {
out := append(append([]int64(nil), a...), b...)
out = uniqueNonZero(out)
sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
return out
}