311 lines
9.5 KiB
Go
311 lines
9.5 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
|
|
}
|
|
if lister, ok := s.channels.(activeChannelBotMemberIDLister); ok {
|
|
memberIDs, err := lister.ListActiveChannelBotMemberIDs(ctx, req.UserID, req.ChannelID, domain.MaxSynchronousChannelDialogFanout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.skippedBotDeliveryUserIDsForIDs(ctx, req, memberIDs)
|
|
}
|
|
memberIDs, err := s.channels.ListActiveChannelMemberIDs(ctx, req.UserID, req.ChannelID, domain.MaxSynchronousChannelDialogFanout)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.skippedBotDeliveryUserIDsForIDs(ctx, req, memberIDs)
|
|
}
|
|
|
|
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
|
|
}
|