Ctrl K
ComunidadGuías…

internal/store/postgres/channel_member_list.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package postgres

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strings"

	"github.com/jackc/pgx/v5"

	"telesrv/internal/domain"
	"telesrv/internal/store"
)

func (s *ChannelStore) GetParticipants(ctx context.Context, viewerUserID, channelID int64, filter domain.ChannelParticipantsFilter, offset, limit int) (domain.ChannelParticipantList, error) {
	channel, viewer, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID)
	if err != nil {
		return domain.ChannelParticipantList{}, err
	}
	if offset < 0 {
		offset = 0
	}
	if offset > domain.MaxChannelParticipantsOffset {
		offset = domain.MaxChannelParticipantsOffset
	}
	if limit <= 0 || limit > domain.MaxChannelParticipantsLimit {
		limit = domain.MaxChannelParticipantsLimit
	}
	count := channel.ParticipantsCount
	// A broadcast channel's subscriber list is enumerable only by admins (the same gate as hidden members): non-admins get only the count,
	// the admins filter is still allowed (the message admin badge data source), and bots return empty.
	if channel.MembersListAdminOnly() && !isChannelAdmin(viewer) {
		switch filter.Kind {
		case domain.ChannelParticipantsAdmins:
		case domain.ChannelParticipantsBots:
			return domain.ChannelParticipantList{Channel: channel, Count: 0}, nil
		default:
			return domain.ChannelParticipantList{Channel: channel, Count: channel.ParticipantsCount}, nil
		}
	}
	where := []string{"m.channel_id = $1"}
	args := []any{channelID}
	joinUsers := false
	query := strings.TrimSpace(filter.Query)
	switch filter.Kind {
	case "", domain.ChannelParticipantsRecent, domain.ChannelParticipantsContacts, domain.ChannelParticipantsMentions:
		where = append(where, "m.status = 'active'")
	case domain.ChannelParticipantsAdmins:
		// Since Layer 225 the admins filter is also the message badge data source: the client pours the whole response
		// (including rank) into the badge cache, so ordinary members bearing a member Tag must also be returned.
		where = append(where, "m.status = 'active'", "(m.role IN ('creator','admin') OR m.rank <> '')")
		count = 0
	case domain.ChannelParticipantsKicked:
		count = channel.KickedCount
		if !isChannelAdmin(viewer) {
			return domain.ChannelParticipantList{Channel: channel, Count: channel.KickedCount}, nil
		}
		where = append(where, "(m.status = 'kicked' OR (m.banned_rights->>'ViewMessages')::boolean IS TRUE)")
	case domain.ChannelParticipantsBanned:
		count = channel.BannedCount
		if !isChannelAdmin(viewer) {
			return domain.ChannelParticipantList{Channel: channel, Count: channel.BannedCount}, nil
		}
		where = append(where, "m.status <> 'kicked'", `(
    m.status = 'banned' OR
    (m.banned_rights->>'SendMessages')::boolean IS TRUE OR
    (m.banned_rights->>'SendMedia')::boolean IS TRUE OR
    (m.banned_rights->>'SendStickers')::boolean IS TRUE OR
    (m.banned_rights->>'SendGifs')::boolean IS TRUE OR
    (m.banned_rights->>'SendGames')::boolean IS TRUE OR
    (m.banned_rights->>'SendInline')::boolean IS TRUE OR
    (m.banned_rights->>'EmbedLinks')::boolean IS TRUE OR
    (m.banned_rights->>'SendPolls')::boolean IS TRUE OR
    (m.banned_rights->>'ChangeInfo')::boolean IS TRUE OR
    (m.banned_rights->>'InviteUsers')::boolean IS TRUE OR
    (m.banned_rights->>'PinMessages')::boolean IS TRUE OR
    (m.banned_rights->>'ManageTopics')::boolean IS TRUE OR
    (m.banned_rights->>'SendPhotos')::boolean IS TRUE OR
    (m.banned_rights->>'SendVideos')::boolean IS TRUE OR
    (m.banned_rights->>'SendRoundvideos')::boolean IS TRUE OR
    (m.banned_rights->>'SendAudios')::boolean IS TRUE OR
    (m.banned_rights->>'SendVoices')::boolean IS TRUE OR
    (m.banned_rights->>'SendDocs')::boolean IS TRUE OR
    (m.banned_rights->>'SendPlain')::boolean IS TRUE OR
    (m.banned_rights->>'EditRank')::boolean IS TRUE OR
    (m.banned_rights->>'SendReactions')::boolean IS TRUE
)`)
	case domain.ChannelParticipantsSearch:
		where = append(where, "m.status = 'active'")
		count = 0
	case domain.ChannelParticipantsBots:
		return domain.ChannelParticipantList{Channel: channel}, nil
	default:
		where = append(where, "m.status = 'active'")
	}
	if !isChannelAdmin(viewer) {
		where = append(where, "NOT (m.role IN ('creator','admin') AND COALESCE((m.admin_rights->>'Anonymous')::boolean, false))")
		if count == channel.ParticipantsCount {
			hiddenCount, err := countHiddenAnonymousChannelAdmins(ctx, s.db, channelID)
			if err != nil {
				return domain.ChannelParticipantList{}, err
			}
			count -= hiddenCount
			if count < 0 {
				count = 0
			}
		}
	}
	if query != "" {
		joinUsers = true
		count = 0
		args = append(args, "%"+strings.ToLower(query)+"%")
		placeholder := fmt.Sprintf("$%d", len(args))
		where = append(where, fmt.Sprintf(`(
    lower(COALESCE(u.first_name, '')) LIKE %s OR
    lower(COALESCE(u.last_name, '')) LIKE %s OR
    lower(COALESCE(u.username, '')) LIKE %s OR
    COALESCE(u.phone, '') LIKE %s OR
    m.user_id::text LIKE %s
)`, placeholder, placeholder, placeholder, placeholder, placeholder))
	}
	args = append(args, offset, limit)
	offsetArg := fmt.Sprintf("$%d", len(args)-1)
	limitArg := fmt.Sprintf("$%d", len(args))
	from := "FROM channel_members m"
	if joinUsers {
		from += " JOIN users u ON u.id = m.user_id"
	}
	rows, err := s.db.Query(ctx, `
SELECT channel_id, user_id, inviter_user_id, role, status, joined_at, left_at, admin_rights::text, banned_rights::text,
       rank, available_min_id, available_min_pts, history_clear_anchor_id, history_clear_anchor_date,
       read_inbox_max_id, read_outbox_max_id, unread_mark, slowmode_last_send_date
`+from+`
WHERE `+strings.Join(where, " AND ")+`
ORDER BY CASE role WHEN 'creator' THEN 0 WHEN 'admin' THEN 1 ELSE 2 END, user_id
OFFSET `+offsetArg+` LIMIT `+limitArg, args...)
	if err != nil {
		return domain.ChannelParticipantList{}, fmt.Errorf("list channel participants: %w", err)
	}
	defer rows.Close()
	out := domain.ChannelParticipantList{Channel: channel, Count: count}
	for rows.Next() {
		member, err := scanChannelMember(rows)
		if err != nil {
			return domain.ChannelParticipantList{}, err
		}
		out.Participants = append(out.Participants, member)
	}
	if err := rows.Err(); err != nil {
		return domain.ChannelParticipantList{}, err
	}
	if out.Count == 0 {
		out.Count = len(out.Participants)
	}
	return out, nil
}

func countHiddenAnonymousChannelAdmins(ctx context.Context, db interface {
	QueryRow(context.Context, string, ...any) pgx.Row
}, channelID int64) (int, error) {
	var count int
	if err := db.QueryRow(ctx, `
SELECT COUNT(*)::int
FROM channel_members
WHERE channel_id = $1
  AND status = 'active'
  AND role IN ('creator','admin')
  AND COALESCE((admin_rights->>'Anonymous')::boolean, false)`, channelID).Scan(&count); err != nil {
		return 0, fmt.Errorf("count hidden anonymous channel admins: %w", err)
	}
	return count, nil
}

func (s *ChannelStore) GetParticipant(ctx context.Context, viewerUserID, channelID, participantUserID int64) (domain.ChannelMember, error) {
	_, viewer, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID)
	if err != nil {
		return domain.ChannelMember{}, err
	}
	if viewerUserID == participantUserID && viewer.Guest {
		return domain.ChannelMember{}, domain.ErrUserNotParticipant
	}
	member, err := s.getChannelMember(ctx, s.db, channelID, participantUserID)
	if errors.Is(err, domain.ErrChannelPrivate) {
		return domain.ChannelMember{}, domain.ErrUserNotParticipant
	}
	if err == nil && participantUserID == viewerUserID && member.Status == domain.ChannelMemberLeft {
		return domain.ChannelMember{}, domain.ErrUserNotParticipant
	}
	return member, err
}

func (s *ChannelStore) ListActiveChannelMemberIDs(ctx context.Context, viewerUserID, channelID int64, limit int) ([]int64, error) {
	if _, _, err := s.getChannelForMember(ctx, s.db, viewerUserID, channelID); err != nil {
		return nil, err
	}
	if limit <= 0 || limit > domain.MaxChannelRealtimeFanout {
		limit = domain.MaxChannelRealtimeFanout
	}
	rows, err := s.db.Query(ctx, `SELECT user_id FROM channel_members WHERE channel_id = $1 AND status = 'active' ORDER BY user_id LIMIT $2`, channelID, limit)
	if err != nil {
		return nil, fmt.Errorf("list active channel members: %w", err)
	}
	defer rows.Close()
	out := make([]int64, 0, limit)
	for rows.Next() {
		var userID int64
		if err := rows.Scan(&userID); err != nil {
			return nil, err
		}
		out = append(out, userID)
	}
	return out, rows.Err()
}

func (s *ChannelStore) ListActiveChannelMembers(ctx context.Context, viewerUserID, channelID int64, limit int) (domain.Channel, domain.ChannelMember, []domain.ChannelMember, error) {
	channel, viewer, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID)
	if err != nil {
		return domain.Channel{}, domain.ChannelMember{}, nil, err
	}
	if limit <= 0 || limit > domain.MaxSynchronousChannelDialogFanout {
		limit = domain.MaxSynchronousChannelDialogFanout
	}
	rows, err := s.db.Query(ctx, `
SELECT channel_id, user_id, inviter_user_id, role, status, joined_at, left_at, admin_rights::text, banned_rights::text,
       rank, available_min_id, available_min_pts, history_clear_anchor_id, history_clear_anchor_date,
       read_inbox_max_id, read_outbox_max_id, unread_mark, slowmode_last_send_date
FROM channel_members
WHERE channel_id = $1 AND status = 'active'
ORDER BY user_id
LIMIT $2`, channelID, limit)
	if err != nil {
		return domain.Channel{}, domain.ChannelMember{}, nil, fmt.Errorf("list active channel members: %w", err)
	}
	defer rows.Close()
	out := make([]domain.ChannelMember, 0, minInt(limit, channel.ParticipantsCount))
	for rows.Next() {
		member, err := scanChannelMember(rows)
		if err != nil {
			return domain.Channel{}, domain.ChannelMember{}, nil, err
		}
		out = append(out, member)
	}
	if err := rows.Err(); err != nil {
		return domain.Channel{}, domain.ChannelMember{}, nil, err
	}
	return channel, viewer, out, nil
}

func (s *ChannelStore) ListActiveChannelBotMembers(ctx context.Context, viewerUserID, channelID int64, offset, limit int) (domain.ChannelParticipantList, error) {
	channel, viewer, _, err := s.getChannelForViewer(ctx, s.db, viewerUserID, channelID)
	if err != nil {
		return domain.ChannelParticipantList{}, err
	}
	if channel.ParticipantsHidden && !isChannelAdmin(viewer) {
		return domain.ChannelParticipantList{Channel: channel, Count: 0}, nil
	}
	if offset < 0 {
		offset = 0
	}
	if offset > domain.MaxChannelParticipantsOffset {
		offset = domain.MaxChannelParticipantsOffset
	}
	if limit <= 0 || limit > domain.MaxChannelParticipantsLimit {
		limit = domain.MaxChannelParticipantsLimit
	}
	rows, err := s.db.Query(ctx, `
SELECT m.channel_id, m.user_id, m.inviter_user_id, m.role, m.status, m.joined_at, m.left_at,
       m.admin_rights::text, m.banned_rights::text, m.rank, m.available_min_id, m.available_min_pts,
       m.history_clear_anchor_id, m.history_clear_anchor_date,
       m.read_inbox_max_id, m.read_outbox_max_id, m.unread_mark, m.slowmode_last_send_date,
       COUNT(*) OVER()::int
FROM bots b
JOIN channel_members m ON m.user_id = b.bot_user_id
WHERE m.channel_id = $1 AND m.status = 'active'
ORDER BY m.user_id
OFFSET $2 LIMIT $3`, channelID, offset, limit)
	if err != nil {
		return domain.ChannelParticipantList{}, fmt.Errorf("list active channel bot members: %w", err)
	}
	defer rows.Close()
	out := domain.ChannelParticipantList{Channel: channel}
	for rows.Next() {
		member, count, err := scanChannelMemberWithCount(rows)
		if err != nil {
			return domain.ChannelParticipantList{}, err
		}
		out.Participants = append(out.Participants, member)
		out.Count = count
	}
	if err := rows.Err(); err != nil {
		return domain.ChannelParticipantList{}, err
	}
	return out, nil
}

func (s *ChannelStore) ListActiveChannelBotMemberIDs(ctx context.Context, viewerUserID, channelID int64, limit int) ([]int64, error) {
	if _, _, err := s.getChannelForMemberOrLinkedGuest(ctx, s.db, viewerUserID, channelID); err != nil {
		return nil, err
	}
	if limit <= 0 || limit > domain.MaxSynchronousChannelDialogFanout {
		limit = domain.MaxSynchronousChannelDialogFanout
	}
	rows, err := s.db.Query(ctx, `
SELECT m.user_id
FROM bots b
JOIN channel_members m ON m.user_id = b.bot_user_id
WHERE m.channel_id = $1 AND m.status = 'active'
ORDER BY m.user_id
LIMIT $2`, channelID, limit)
	if err != nil {
		return nil, fmt.Errorf("list active channel bot member ids: %w", err)
	}
	defer rows.Close()
	out := make([]int64, 0, minInt(limit, domain.MaxChannelParticipantsLimit))
	for rows.Next() {
		var userID int64
		if err := rows.Scan(&userID); err != nil {
			return nil, err
		}
		out = append(out, userID)
	}
	return out, rows.Err()
}

func (s *ChannelStore) FilterActiveChannelMemberIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error) {
	if channelID == 0 || len(userIDs) == 0 {
		return nil, nil
	}
	candidates := uniqueChannelUserIDs(userIDs, 0)
	if len(candidates) == 0 {
		return nil, nil
	}
	out := make([]int64, 0, len(candidates))
	for start := 0; start < len(candidates); start += channelMemberFilterBatch {
		end := start + channelMemberFilterBatch
		if end > len(candidates) {
			end = len(candidates)
		}
		rows, err := s.db.Query(ctx, `
SELECT user_id
FROM channel_members
WHERE channel_id = $1
  AND user_id = ANY($2::bigint[])
  AND status = 'active'
ORDER BY user_id`, channelID, candidates[start:end])
		if err != nil {
			return nil, fmt.Errorf("filter active channel members: %w", err)
		}
		for rows.Next() {
			var userID int64
			if err := rows.Scan(&userID); err != nil {
				rows.Close()
				return nil, err
			}
			out = append(out, userID)
		}
		if err := rows.Err(); err != nil {
			rows.Close()
			return nil, err
		}
		rows.Close()
	}
	sort.Slice(out, func(i, j int) bool { return out[i] < out[j] })
	return out, nil
}

func (s *ChannelStore) FilterActiveChannelMemberPairs(ctx context.Context, userIDsByChannel map[int64][]int64) (map[int64][]int64, error) {
	channelIDs, userIDs, err := flattenActiveChannelMemberPairs(userIDsByChannel)
	if err != nil {
		return nil, err
	}
	out := make(map[int64][]int64)
	if len(channelIDs) == 0 {
		return out, nil
	}
	rows, err := s.db.Query(ctx, `
WITH requested(channel_id, user_id) AS (
  SELECT * FROM unnest($1::bigint[], $2::bigint[])
)
SELECT r.channel_id, r.user_id
FROM requested r
JOIN channel_members m
  ON m.channel_id = r.channel_id
 AND m.user_id = r.user_id
WHERE m.status = 'active'
ORDER BY r.channel_id, r.user_id`, channelIDs, userIDs)
	if err != nil {
		return nil, fmt.Errorf("filter active channel member pairs: %w", err)
	}
	defer rows.Close()
	for rows.Next() {
		var channelID, userID int64
		if err := rows.Scan(&channelID, &userID); err != nil {
			return nil, err
		}
		out[channelID] = append(out[channelID], userID)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return out, nil
}

func flattenActiveChannelMemberPairs(userIDsByChannel map[int64][]int64) ([]int64, []int64, error) {
	channelIDs := make([]int64, 0)
	userIDs := make([]int64, 0)
	seen := make(map[[2]int64]struct{})
	for channelID, candidates := range userIDsByChannel {
		if channelID == 0 {
			continue
		}
		for _, userID := range candidates {
			if userID == 0 {
				continue
			}
			pair := [2]int64{channelID, userID}
			if _, ok := seen[pair]; ok {
				continue
			}
			if len(seen) >= store.MaxActiveChannelMemberPairs {
				return nil, nil, fmt.Errorf("%w: maximum %d", store.ErrActiveChannelMemberPairsLimit, store.MaxActiveChannelMemberPairs)
			}
			seen[pair] = struct{}{}
			channelIDs = append(channelIDs, channelID)
			userIDs = append(userIDs, userID)
		}
	}
	return channelIDs, userIDs, nil
}

func (s *ChannelStore) FilterChannelMessageAudienceIDs(ctx context.Context, channelID int64, userIDs []int64) ([]int64, error) {
	if channelID == 0 || len(userIDs) == 0 {
		return nil, nil
	}
	candidates := uniqueChannelUserIDs(userIDs, 0)
	if len(candidates) == 0 {
		return nil, nil
	}
	out := make([]int64, 0, len(candidates))
	for start := 0; start < len(candidates); start += channelMemberFilterBatch {
		end := start + channelMemberFilterBatch
		if end > len(candidates) {
			end = len(candidates)
		}
		rows, err := s.db.Query(ctx, `
SELECT candidate.user_id
FROM channels c
CROSS JOIN unnest($2::bigint[]) AS candidate(user_id)
LEFT JOIN channel_members m
  ON m.channel_id = c.id AND m.user_id = candidate.user_id
WHERE c.id = $1
  AND NOT c.deleted
  AND NOT COALESCE((m.banned_rights->>'ViewMessages')::boolean, false)
  AND COALESCE(m.status, '') NOT IN ('kicked', 'banned')
  AND (
    m.status = 'active'
    OR (COALESCE(c.username, '') <> '' AND COALESCE(m.status, 'left') = 'left')
  )
ORDER BY candidate.user_id`, channelID, candidates[start:end])
		if err != nil {
			return nil, fmt.Errorf("filter channel message audience: %w", err)
		}
		for rows.Next() {
			var userID int64
			if err := rows.Scan(&userID); err != nil {
				rows.Close()
				return nil, err
			}
			out = append(out, userID)
		}
		if err := rows.Err(); err != nil {
			rows.Close()
			return nil, err
		}
		rows.Close()
	}
	return out, nil
}